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:
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:
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:
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
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
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
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:
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:
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:
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 MCPCase 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
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:
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:
microsoft_docs_search tool with a query parameter to retrieve documentationThis scenario demonstrates how to:
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.
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 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:
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.
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()
scenario2.py.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:
You'll see how to:
.vscode/mcp.json file to your workspace root (see example below).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:
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:
What's Next
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:
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 MCPCase 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
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:
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:
microsoft_docs_search tool with a query parameter to retrieve documentationThis scenario demonstrates how to:
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.
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 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:
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.
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()
scenario2.py.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:
You'll see how to:
.vscode/mcp.json file to your workspace root (see example below).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:
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:
What's Next
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.
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 CodeCase 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
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:
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:
microsoft_docs_search tool with a query parameter to retrieve documentationThis scenario demonstrates how to:
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.
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 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:
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.
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()
scenario2.py.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:
You'll see how to:
.vscode/mcp.json file to your workspace root (see example below).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:
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:
What's Next
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:
The implementation includes:
.vscode/mcp.json configuration for easy setupThis 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:
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:
Solution Architecture
GitHub's MCP Registry centralizes trusted MCP servers with key features:
Business Impact
The registry has delivered measurable improvements:
github-mcp-server, enabling natural language GitHub automation (PR creation, CI reruns, code scanning)Strategic Value
For practitioners specializing in agent lifecycle management and reproducible workflows, the MCP Registry provides:
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:
By studying these implementations, you gain critical insights into:
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
What's Next
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:
๐ 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:
๐ฏ 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:
๐ก Development Benefits:
๐ 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
๐ Module 1: AI Toolkit Fundamentals
๐ Learning Objectives
By the end of this module, you will be able to:
๐ง 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
๐ฏ Why AI Toolkit Matters
๐ ๏ธ 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
Step 3: Install and Activate
โ Verification Checklist
๐งช 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:
๐ง 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:
๐ฏ 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:
๐ค 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:
โ๏ธ 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:
๐ 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:
๐ Module 1 Wrap-Up & Next Steps
๐ Knowledge Check
Test your understanding:
๐ Additional Resources
๐ 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:
Duration: 15 minutes
๐ฏ 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
๐ Module 2: MCP with AI Toolkit Fundamentals
๐ Learning Objectives
By the end of this module, you will be able to:
๐ฏ 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:
๐ง 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:
With MCP:
๐๏ธ 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:
๐ข 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:
๐ Use Cases:
2. ๐ Microsoft Dataverse MCP
๐ Documentation: Microsoft Dataverse Integration
๐ฏ Purpose: Natural language interface for business data
โจ Key Features:
๐ Use Cases:
3. ๐ Playwright MCP Server
๐ Repository: microsoft/playwright-mcp
๐ฏ Purpose: Browser automation and web interaction capabilities
โจ Key Features:
๐ Use Cases:
4. ๐ Files MCP Server
๐ Repository: microsoft/files-mcp-server
๐ฏ Purpose: Intelligent file system operations
โจ Key Features:
๐ Use Cases:
5. ๐ MarkItDown MCP Server
๐ Repository: microsoft/markitdown
๐ฏ Purpose: Advanced Markdown processing and manipulation
โจ Key Features:
๐ Use Cases:
6. ๐ Clarity MCP Server
๐ฆ Package: @microsoft/clarity-mcp-server
๐ฏ Purpose: Web analytics and user behavior insights
โจ Key Features:
๐ Use Cases:
๐ Community Ecosystem
Beyond Microsoft's servers, the MCP ecosystem includes:
๐ ๏ธ 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:
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:
goto, goBack, goForward, reloadclick, fill, press, hover, dragtextContent, innerHTML, getAttributeisVisible, isEnabled, waitForSelectorscreenshot, pdf, videosetExtraHTTPHeaders, route, waitForResponseStep 7: Verify Integration Success
โ Success Indicators:
๐ง Troubleshooting Common Issues:
๐ฏ 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:
๐ Additional Resources
๐ 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:
Duration: 20 minutes
๐ฏ 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
๐ง Module 3: Advanced MCP Development with AI Toolkit
๐ฏ Learning Objectives
By the end of this lab, you will be able to:
๐ Prerequisites
๐๏ธ What You'll Build
In this lab, you'll create a Weather MCP Server that demonstrates:
---
๐ง 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:
---
๐ 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:
๐ง Advanced Features Explored
๐ Additional Resources
---
๐ 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:
Duration: 20 minutes
๐ฏ 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
๐ 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:
๐ Prerequisites
๐๏ธ 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:
---
๐ 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:
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:
---
๐ 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:
๐ฎ 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!
Duration: 30 minutes
๐ฏ 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:
๐งช Quality Assurance Revolution
Elevate testing with AI-powered automation:
๐ Data Pipeline Intelligence
Build smarter data processing workflows:
๐ง Customer Experience Enhancement
Create exceptional customer interactions:
๐ ๏ธ Prerequisites & Setup
๐ป System Requirements
๐ง Development Environment
Recommended VS Code Extensions
Optional Tools
๐๏ธ Learning Outcomes & Certification Path
๐ Skill Mastery Checklist
By completing this workshop, you will achieve mastery in:
๐ฏ Core Competencies
๐ง Technical Skills
๐ Advanced Capabilities
๐ Additional Resources
---
๐ 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
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 Database Integration Learning Path
๐ Complete Learning Structure for https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail
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:
๐งญ 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:
The MCP Solution
Model Context Protocol addresses these challenges by providing:
๐ช 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:
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:
๐๏ธ 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
2. Database Integration Layer
3. Security Layer
4. AI Enhancement Layer
๐ง Technology Stack
Core Technologies
Development Tools
Production Stack
๐ฌ 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:
Data Protection
Multiple layers of security:
๐ฏ 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
Database Integration
Azure Services
---
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.
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:
๐งญ 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:
The MCP Solution
Model Context Protocol addresses these challenges by providing:
๐ช 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:
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:
๐๏ธ 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
2. Database Integration Layer
3. Security Layer
4. AI Enhancement Layer
๐ง Technology Stack
Core Technologies
Development Tools
Production Stack
๐ฌ 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:
Data Protection
Multiple layers of security:
๐ฏ 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
Database Integration
Azure Services
---
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.
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:
๐๏ธ 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:
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:
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:
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:
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:
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:
๐ Additional Resources
Architecture Patterns
PostgreSQL Advanced Topics
Python Async Patterns
---
Next: Ready to explore security patterns? Continue with Lab 02: Security and Multi-Tenancy
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:
๐๏ธ 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:
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:
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:
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:
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:
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:
๐ Additional Resources
Architecture Patterns
PostgreSQL Advanced Topics
Python Async Patterns
---
Next: Ready to explore security patterns? Continue with Lab 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:
๐ 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:
Trade-offs:
๐ก๏ธ 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:
๐ Additional Resources
Azure Security
Database Security
Security Testing
---
Previous: Lab 01: Core Architecture Concepts
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:
๐ 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:
Trade-offs:
๐ก๏ธ 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:
๐ Additional Resources
Azure Security
Database Security
Security Testing
---
Previous: Lab 01: Core Architecture Concepts
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:
๐ Prerequisites Check
Before starting, ensure you have:
Required Knowledge
System Requirements
Account Requirements
๐ ๏ธ 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:
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
โ Azure Resources
โ Environment Configuration
.env file created with all required variablesaz account show)โ VS Code Integration
.vscode/mcp.json configured๐ ๏ธ 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:
๐ Additional Resources
Development Tools
Azure Services
Python Development
---
Next: Environment ready? Continue with Lab 04: Database Design and Schema
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:
๐ Prerequisites Check
Before starting, ensure you have:
Required Knowledge
System Requirements
Account Requirements
๐ ๏ธ 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:
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
โ Azure Resources
โ Environment Configuration
.env file created with all required variablesaz account show)โ VS Code Integration
.vscode/mcp.json configured๐ ๏ธ 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:
๐ Additional Resources
Development Tools
Azure Services
Python Development
---
Next: Environment ready? Continue with Lab 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:
๐๏ธ 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:
๐ Additional Resources
PostgreSQL & pgvector
Multi-Tenant Architecture
Vector Databases
---
Previous: Lab 03: Environment Setup
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:
๐๏ธ 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:
๐ Additional Resources
PostgreSQL & pgvector
Multi-Tenant Architecture
Vector Databases
---
Previous: Lab 03: Environment Setup
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:
๐ 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
๐๏ธ 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
๐ง 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
๐ฅ 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:
๐ Additional Resources
FastMCP Framework
Database Integration
FastAPI Patterns
---
Next: Ready to expand your tools? Continue with Lab 06: Tool Development
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:
๐ 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
๐๏ธ 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
๐ง 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
๐ฅ 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:
๐ Additional Resources
FastMCP Framework
Database Integration
FastAPI Patterns
---
Next: Ready to expand your tools? Continue with Lab 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:
๐ ๏ธ 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:
๐ Additional Resources
MCP Tool Development
Database Security
Performance Optimization
---
Previous: Lab 05: MCP Server Implementation
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:
๐ ๏ธ 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:
๐ Additional Resources
MCP Tool Development
Database Security
Performance Optimization
---
Previous: Lab 05: MCP Server Implementation
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:
๐ง 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:
๐ Additional Resources
Azure OpenAI
Vector Databases
Semantic Search
---
Previous: Lab 06: Tool Development
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:
๐ง 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:
๐ Additional Resources
Azure OpenAI
Vector Databases
Semantic Search
---
Previous: Lab 06: Tool Development
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:
๐งช 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:
๐ Additional Resources
Testing Frameworks
Performance Testing
Debugging Tools
---
Previous: Lab 07: Semantic Search Integration
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:
๐งช 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:
๐ Additional Resources
Testing Frameworks
Performance Testing
Debugging Tools
---
Previous: Lab 07: Semantic Search 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:
๐ง 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:
๐ Additional Resources
VS Code Development
MCP Protocol
Development Tools
---
Previous: Lab 08: Testing and Debugging
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:
๐ง 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:
๐ Additional Resources
VS Code Development
MCP Protocol
Development Tools
---
Previous: Lab 08: Testing and Debugging
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:
๐ณ 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:
๐ Additional Resources
Container Technologies
CI/CD and DevOps
Security and Monitoring
---
Previous: Lab 09: VS Code Integration
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:
๐ณ 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:
๐ Additional Resources
Container Technologies
CI/CD and DevOps
Security and Monitoring
---
Previous: Lab 09: VS Code Integration
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:
๐ 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:
๐ Additional Resources
Azure Monitor
OpenTelemetry
Operational Excellence
---
Previous: Lab 10: Deployment Strategies
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:
๐ 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:
๐ Additional Resources
Azure Monitor
OpenTelemetry
Operational Excellence
---
Previous: Lab 10: Deployment Strategies
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:
๐ 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:
Advanced Learning Paths
Continue your MCP journey with:
Community Recognition
Share your achievement:
๐ Additional Resources
Advanced Topics
Security Resources
Community
---
๐ 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.
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:
๐ 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:
Advanced Learning Paths
Continue your MCP journey with:
Community Recognition
Share your achievement:
๐ Additional Resources
Advanced Topics
Security Resources
Community
---
๐ 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:
๐ฏ Prerequisites for Learning
To get the most out of this learning path, you should have:
Required Tools
๐ Study Guide & Resources
This learning path includes comprehensive resources to help you navigate effectively:
Study Guide
Each lab includes:
Prerequisites Check
Before starting each lab, you'll find:
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:
Hands-on Practice
Each lab includes:
๐ Community and Support
Get Help
๐ Ready to Start?
Begin your journey with Lab 00: Introduction to MCP Database Integration 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. 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. By the end of this lab, you will be able to: Modern AI assistants are incredibly powerful but face significant limitations when working with real-world business data: Model Context Protocol addresses these challenges by providing: 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. Zava Retail operates: 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 The MCP server must provide: Our MCP server implements a layered architecture optimized for database integration: Let's explore how different users interact with our MCP server: 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 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 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 Our implementation prioritizes enterprise-grade security: PostgreSQL RLS ensures data isolation: Each MCP connection includes: Multiple layers of security: 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 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. --- 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.Introduction to MCP Database Integration
๐ฏ What This Lab Covers
Overview
Learning Objectives
๐งญ The Challenge: AI Meets Real-World Data
Traditional AI Limitations
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
๐ช Meet Zava Retail: Our Learning Case Study https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail
Business Context
Business Requirements
Technical Requirements
๐๏ธ MCP Server Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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
2. Database Integration Layer
3. Security Layer
4. AI Enhancement Layer
๐ง 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
Scenario 1: Store Manager Performance Review
Scenario 2: Product Discovery with Semantic Search
Scenario 3: Cross-Store Analytics
๐ Security and Multi-Tenancy Deep Dive
Row Level Security (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()));
User Identity Management
Data Protection
๐ฏ Key Takeaways
๐ What's Next
๐ Additional Resources
MCP Documentation
Database Integration
Azure Services
---
*Master building production-ready MCP servers with database integration through this comprehensive, hands-on learning experience.*
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:
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.*
Module 08 — ๋ชจ๋ฒ ์ฌ๋ก
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 ๋ชจ๋ฒ ์ฌ๋ก ์ ๋ณด๋ ๋ค์์ ์ฐธ์กฐํ์ธ์:
์ค์ฉ์ ๊ตฌํ ์์
๋๊ตฌ ์ค๊ณ ๋ชจ๋ฒ ์ฌ๋ก
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 ์๋ฒ์ ๊ฐ๋ณ ๊ตฌ์ฑ ์์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฒ์ฆํฉ๋๋ค.
ํ ์คํธ ๋์
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๋ฅผ ์ด์ฉํ ๊ธฐ๋ณธ ๋ถํ ํ ์คํธ
// 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. ํ ์คํธ ์ผ์ด์ค ๋ฌธ์ํ: ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ๋ช ํํ๊ฒ ๋ฌธ์ํ ์ ์ง
์ผ๋ฐ์ ์ธ ํ ์คํธ ํจ์
๊ฒฐ๋ก
์ ๋ขฐํ ์ ์๊ณ ๊ณ ํ์ง์ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์์ ์๋ ค๋๋ฆฝ๋๋ค.
์๋ฌธ ๋ฌธ์๋ ํด๋น ์ธ์ด์ ๊ณต์ ์๋ฃ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
Module 09 — ์ฌ๋ก ์ฐ๊ตฌ
MCP ์คํ ์ฌ๋ก: ์ค์ ์ฌ๋ก ์ฐ๊ตฌ
_(์ ์ด๋ฏธ์ง๋ฅผ ํด๋ฆญํ๋ฉด ์ด ์์ ์ ๋์์์ ๋ณผ ์ ์์ต๋๋ค)_
Model Context Protocol(MCP)์ AI ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ฐ์ดํฐ, ๋๊ตฌ ๋ฐ ์๋น์ค์ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ํ์ ํ๊ณ ์์ต๋๋ค. ์ด ์น์ ์์๋ ๋ค์ํ ๊ธฐ์ ์๋๋ฆฌ์ค์์ MCP์ ์ค์ ์ ์ฉ ์ฌ๋ก๋ฅผ ๋ณด์ฌ์ค๋๋ค.
๊ฐ์
์ด ์น์ ์์๋ MCP ๊ตฌํ์ ๊ตฌ์ฒด์ ์ธ ์๋ฅผ ํตํด ์กฐ์ง๋ค์ด ์ด ํ๋กํ ์ฝ์ ํ์ฉํด ๋ณต์กํ ๋น์ฆ๋์ค ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ๊ณ ์๋์ง ์๊ฐํฉ๋๋ค. ์ฌ๋ก ์ฐ๊ตฌ๋ฅผ ์ดํด๋ณด๋ฉด ์ค์ ์ํฉ์์ MCP์ ๋ค์ฌ๋ค๋ฅํจ, ํ์ฅ์ฑ, ์ค์ง์ ์ด์ ์ ๋ํ ํต์ฐฐ์ ์ป์ ์ ์์ต๋๋ค.
์ฃผ์ ํ์ต ๋ชฉํ
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ค์ ํ๊ตฌํจ์ผ๋ก์จ ์ฌ๋ฌ๋ถ์:
์ฃผ์ ์ฌ๋ก ์ฐ๊ตฌ
1. Azure AI ์ฌํ์ฌ โ ์ฐธ์กฐ ๊ตฌํ
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ MCP, Azure OpenAI, Azure AI Search๋ฅผ ์ฌ์ฉํด ๋ค์ค ์์ด์ ํธ ๊ธฐ๋ฐ AI ์ฌํ ๊ณํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ ๋ง์ดํฌ๋ก์ํํธ์ ํฌ๊ด์ ์ฐธ์กฐ ์๋ฃจ์ ์ ๋ค๋ฃน๋๋ค. ํ๋ก์ ํธ๋ ๋ค์์ ๋ณด์ฌ์ค๋๋ค:
์ํคํ ์ฒ ๋ฐ ๊ตฌํ ์ธ๋ถ์ฌํญ์ MCP๋ฅผ ์กฐ์จ ๊ณ์ธต์ผ๋ก ํ์ฉํ ๋ณต์กํ ๋ค์ค ์์ด์ ํธ ์์คํ ๊ตฌ์ถ์ ๋ํ ์ ์ฉํ ํต์ฐฐ์ ์ ๊ณตํฉ๋๋ค.
2. YouTube ๋ฐ์ดํฐ๋ก Azure DevOps ํญ๋ชฉ ์ ๋ฐ์ดํธ
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ MCP๋ฅผ ์ด์ฉํด ์ํฌํ๋ก์ฐ ์๋ํ๋ฅผ ์ค์ฉ์ ์ผ๋ก ์ ์ฉํ ์๋ฅผ ๋ณด์ฌ์ค๋๋ค. MCP ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ:
์ด ์์ ๋ ๋น๊ต์ ๋จ์ํ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋
ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
์ฌ๋ก ์ฐ๊ตฌ: ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๊ธฐ
์ฝ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๊ณ ํ ๋ ๋ฌธ์ ์ฌ์ดํธ, Stack Overflow, ์๋ง์ ๊ฒ์ ์์ง ํญ ์ฌ์ด๋ฅผ ๋ฐ์๊ฒ ์ค๊ฐ ๋ณธ ์ ์ด ์๋์?
ํน์ ๋ฌธ์ ์ ์ฉ์ผ๋ก ๋ ๋ฒ์งธ ๋ชจ๋ํฐ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ IDE์ ๋ธ๋ผ์ฐ์ ์ฌ์ด๋ฅผ ๊ณ์ํด์ Alt+Tab ํ๋ ๊ฒฝ์ฐ๋ ์์ ๊ฒ๋๋ค.
๋ฌธ์๋ฅผ ์ํฌํ๋ก์ฐ ์์์โ์ฑ, IDE ๋๋ ๋ง์ถค ๋๊ตฌ์ ํตํฉํด์โ๋ฐ๋ก ํ์ธํ ์ ์๋ค๋ฉด ํจ์ฌ ๋ซ์ง ์์๊น์?
์ด๋ฒ ์ฌ๋ก ์ฐ๊ตฌ์์๋ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ง์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.
๊ฐ์
ํ๋ ๊ฐ๋ฐ์ ๋จ์ง ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์๋๋ผ, ์ ์์ ์ ์ ํ ์ ๋ณด๋ฅผ ์ฐพ๋ ์ผ์ ๋๋ค.
๋ฌธ์๋ ์ด๋์๋ ์กด์ฌํ์ง๋ง, ๊ฐ์ฅ ํ์ํ ๋์ธ ๋๊ตฌ์ ์ํฌํ๋ก์ฐ ๋ด๋ถ์ ์๋ ๊ฒฝ์ฐ๋ ๋๋ญ ๋๋ค.
๋ฌธ์ ๊ฒ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ง์ ํตํฉํ๋ฉด ์๊ฐ์ ์ ์ฝํ๊ณ , ์ปจํ ์คํธ ์ ํ์ ์ค์ด๋ฉฐ ์์ฐ์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
์ด ์น์ ์์๋ ํด๋ผ์ด์ธํธ๊ฐ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ์ ์๋ดํฉ๋๋ค.
์ฐ๊ฒฐ์ ์ค์ ํ๊ณ , ์์ฒญ์ ๋ณด๋ด๋ฉฐ, ์คํธ๋ฆฌ๋ฐ ์๋ต์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์ค๋ช ํฉ๋๋ค. ์ด ์ ๊ทผ๋ฒ์ ์ํฌํ๋ก์ฐ๋ฅผ ๊ฐ์ํํ ๋ฟ ์๋๋ผ, ๋ ๋๋ํ๊ณ ๋์์ด ๋๋ ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ๊ฐ๋ฅ์ฑ์ ์ด์ด์ค๋๋ค.
ํ์ต ๋ชฉํ
์ ์ด ์์ ์ ํ ๊น์? ์ต๊ณ ์ ๊ฐ๋ฐ์ ๊ฒฝํ์ ๋ง์ฐฐ์ ์ ๊ฑฐํ๋ ๋ฐ ์์ต๋๋ค. ์ฝ๋ ํธ์ง๊ธฐ, ์ฑ๋ด ๋๋ ์น ์ฑ์ด Microsoft Learn์ ์ต์ ์ฝํ ์ธ ๋ฅผ ์ฌ์ฉํด ๋ฌธ์ ์ง๋ฌธ์ ์ฆ์ ๋ตํ ์ ์๋ค๊ณ ์์ํด ๋ณด์ธ์. ์ด ์ฅ์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
์ด ๊ธฐ์ ๋ค์ ํตํด ๋ฐ์ํ ๋ฟ๋ง ์๋๋ผ ์ง์ ์ผ๋ก ๋ํํ์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์์ด ๊ฐ๋ฅํ ๋๊ตฌ๋ฅผ ๋ง๋๋ ๋ฒ์ ๋ฐฐ์ฐ์ค ๊ฒ๋๋ค.
์๋๋ฆฌ์ค 1 - MCP๋ฅผ ์ด์ฉํ ์ค์๊ฐ ๋ฌธ์ ๊ฒ์
์ด ์๋๋ฆฌ์ค์์๋ ํด๋ผ์ด์ธํธ๋ฅผ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค. ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ ์ ์์ต๋๋ค.
์ค์ต์ ์์ํด ๋ด
์๋ค. Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ microsoft_docs_search ๋๊ตฌ๋ฅผ ํธ์ถํ๊ณ , ์คํธ๋ฆฌ๋ฐ ์๋ต์ ์ฝ์์ ๊ธฐ๋กํ๋ ์ฑ์ ์์ฑํ๋ ๊ฒ์ด ๊ณผ์ ์
๋๋ค.
์ ์ด ๋ฐฉ๋ฒ์ธ๊ฐ์?
์ด๊ฒ์ด ์ฑ๋ด, IDE ํ์ฅ, ์น ๋์๋ณด๋์ ๊ฐ์ ๊ณ ๊ธ ํตํฉ ๊ธฐ๋ฅ์ ๊ตฌ์ถํ๋ ํ ๋๊ฐ ๋๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด ์๋๋ฆฌ์ค์ ์ฝ๋์ ์ง์นจ์ ์ด ์ฌ๋ก ์ฐ๊ตฌ ๋ด solution ํด๋์์ ์ฐพ์ ์ ์์ต๋๋ค. ๋ค์ ๋จ๊ณ์ ๋ฐ๋ผ ์ฐ๊ฒฐ์ ์ค์ ํ์ธ์:
microsoft_docs_search ๋๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์์ ํจ๊ป ํธ์ถํ์ฌ ๋ฌธ์ ๊ฐ์ ธ์ค๊ธฐ์ด ์๋๋ฆฌ์ค๋ ๋ค์์ ์์ฐํฉ๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
Prompt> What is Azure Key Vault?
Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
์๋๋ ์ต์ ์ํ ์๋ฃจ์ ์ ๋๋ค. ์ ์ฒด ์ฝ๋์ ์์ธํ ๋ด์ฉ์ solution ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ๋ฌธ์๋ฅผ ๊ฒ์ํ ์ ์๊ฒ ํ์ฌ, ์ฑ์ด๋ ์ฌ์ดํธ ๋ด์์ ์ฆ์ ๋ฌธ์ ์ ๊ทผ์ ๊ฐ๋ฅํ๊ฒ ํ๋ ๊ฒ์ ๋๋ค.
๋ค์ ๋ด์ฉ์ ๋ฐฐ์ธ ์ ์์ต๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
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 ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ์๋ฒ๋ฅผ ์๋ํฐ ๋ด์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ค์์ด ๊ฐ๋ฅํฉ๋๋ค:
๋ค์ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค:
.vscode/mcp.json ํ์ผ ์ถ๊ฐ(์์๋ ์๋ ์ฐธ์กฐ)VS Code์์ MCP ์๋ฒ ์ค์ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
{
"servers": {
"LearnDocsMCP": {
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
> ๋จ๊ณ๋ณ ๊ฐ์ด๋์ ์คํฌ๋ฆฐ์ท์ด ํฌํจ๋ ์์ธํ ์ค๋ช
์ README.md๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด ๋ฐฉ๋ฒ์ ๊ธฐ์ ๊ฐ์ข๋ฅผ ๋ง๋ค๊ฑฐ๋ ๋ฌธ์๋ฅผ ์์ฑํ๊ฑฐ๋ ๋น๋ฒํ๊ฒ ์ฐธ์กฐ๊ฐ ํ์ํ ์ฝ๋๋ฅผ ๊ฐ๋ฐํ๋ ๋ชจ๋ ์ฌ๋์๊ฒ ์ด์์ ์ ๋๋ค.
์ฃผ์ ์์
๋ฌธ์๋ฅผ ๋๊ตฌ์ ์ง์ ํตํฉํ๋ ๊ฒ์ ๋จ์ํ ํธ์๊ฐ ์๋๋ผ ์์ฐ์ฑ์ ํ๋ช ์ ๋๋ค. ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ฉด:
์ด ๊ธฐ์ ๋ค์ ํจ์จ์ ์ผ ๋ฟ ์๋๋ผ ์ฌ์ฉํ๊ธฐ ์ฆ๊ฑฐ์ด ์๋ฃจ์ ์ ๋ง๋๋ ๋ฐ ๋์์ ์ค ๊ฒ์ ๋๋ค.
์ถ๊ฐ ์๋ฃ
์ดํด๋ฅผ ๊น๊ฒ ํ๋ ค๋ฉด ๋ค์ ๊ณต์ ์์์ ํ์ํด ๋ณด์ธ์:
๋ค์ ๋จ๊ณ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์ด ์ฌ๋ก ์ฐ๊ตฌ์์๋ Python ์ฝ์ ํด๋ผ์ด์ธํธ๋ฅผ MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ์ค์๊ฐ์ผ๋ก ์ปจํ ์คํธ ์ธ์ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋
ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
์ฌ๋ก ์ฐ๊ตฌ: ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๊ธฐ
์ฝ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๊ณ ํ ๋ ๋ฌธ์ ์ฌ์ดํธ, Stack Overflow, ์๋ง์ ๊ฒ์ ์์ง ํญ ์ฌ์ด๋ฅผ ๋ฐ์๊ฒ ์ค๊ฐ ๋ณธ ์ ์ด ์๋์?
ํน์ ๋ฌธ์ ์ ์ฉ์ผ๋ก ๋ ๋ฒ์งธ ๋ชจ๋ํฐ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ IDE์ ๋ธ๋ผ์ฐ์ ์ฌ์ด๋ฅผ ๊ณ์ํด์ Alt+Tab ํ๋ ๊ฒฝ์ฐ๋ ์์ ๊ฒ๋๋ค.
๋ฌธ์๋ฅผ ์ํฌํ๋ก์ฐ ์์์โ์ฑ, IDE ๋๋ ๋ง์ถค ๋๊ตฌ์ ํตํฉํด์โ๋ฐ๋ก ํ์ธํ ์ ์๋ค๋ฉด ํจ์ฌ ๋ซ์ง ์์๊น์?
์ด๋ฒ ์ฌ๋ก ์ฐ๊ตฌ์์๋ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ง์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.
๊ฐ์
ํ๋ ๊ฐ๋ฐ์ ๋จ์ง ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์๋๋ผ, ์ ์์ ์ ์ ํ ์ ๋ณด๋ฅผ ์ฐพ๋ ์ผ์ ๋๋ค.
๋ฌธ์๋ ์ด๋์๋ ์กด์ฌํ์ง๋ง, ๊ฐ์ฅ ํ์ํ ๋์ธ ๋๊ตฌ์ ์ํฌํ๋ก์ฐ ๋ด๋ถ์ ์๋ ๊ฒฝ์ฐ๋ ๋๋ญ ๋๋ค.
๋ฌธ์ ๊ฒ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ง์ ํตํฉํ๋ฉด ์๊ฐ์ ์ ์ฝํ๊ณ , ์ปจํ ์คํธ ์ ํ์ ์ค์ด๋ฉฐ ์์ฐ์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
์ด ์น์ ์์๋ ํด๋ผ์ด์ธํธ๊ฐ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ์ ์๋ดํฉ๋๋ค.
์ฐ๊ฒฐ์ ์ค์ ํ๊ณ , ์์ฒญ์ ๋ณด๋ด๋ฉฐ, ์คํธ๋ฆฌ๋ฐ ์๋ต์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์ค๋ช ํฉ๋๋ค. ์ด ์ ๊ทผ๋ฒ์ ์ํฌํ๋ก์ฐ๋ฅผ ๊ฐ์ํํ ๋ฟ ์๋๋ผ, ๋ ๋๋ํ๊ณ ๋์์ด ๋๋ ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ๊ฐ๋ฅ์ฑ์ ์ด์ด์ค๋๋ค.
ํ์ต ๋ชฉํ
์ ์ด ์์ ์ ํ ๊น์? ์ต๊ณ ์ ๊ฐ๋ฐ์ ๊ฒฝํ์ ๋ง์ฐฐ์ ์ ๊ฑฐํ๋ ๋ฐ ์์ต๋๋ค. ์ฝ๋ ํธ์ง๊ธฐ, ์ฑ๋ด ๋๋ ์น ์ฑ์ด Microsoft Learn์ ์ต์ ์ฝํ ์ธ ๋ฅผ ์ฌ์ฉํด ๋ฌธ์ ์ง๋ฌธ์ ์ฆ์ ๋ตํ ์ ์๋ค๊ณ ์์ํด ๋ณด์ธ์. ์ด ์ฅ์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
์ด ๊ธฐ์ ๋ค์ ํตํด ๋ฐ์ํ ๋ฟ๋ง ์๋๋ผ ์ง์ ์ผ๋ก ๋ํํ์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์์ด ๊ฐ๋ฅํ ๋๊ตฌ๋ฅผ ๋ง๋๋ ๋ฒ์ ๋ฐฐ์ฐ์ค ๊ฒ๋๋ค.
์๋๋ฆฌ์ค 1 - MCP๋ฅผ ์ด์ฉํ ์ค์๊ฐ ๋ฌธ์ ๊ฒ์
์ด ์๋๋ฆฌ์ค์์๋ ํด๋ผ์ด์ธํธ๋ฅผ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค. ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ ์ ์์ต๋๋ค.
์ค์ต์ ์์ํด ๋ด
์๋ค. Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ microsoft_docs_search ๋๊ตฌ๋ฅผ ํธ์ถํ๊ณ , ์คํธ๋ฆฌ๋ฐ ์๋ต์ ์ฝ์์ ๊ธฐ๋กํ๋ ์ฑ์ ์์ฑํ๋ ๊ฒ์ด ๊ณผ์ ์
๋๋ค.
์ ์ด ๋ฐฉ๋ฒ์ธ๊ฐ์?
์ด๊ฒ์ด ์ฑ๋ด, IDE ํ์ฅ, ์น ๋์๋ณด๋์ ๊ฐ์ ๊ณ ๊ธ ํตํฉ ๊ธฐ๋ฅ์ ๊ตฌ์ถํ๋ ํ ๋๊ฐ ๋๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด ์๋๋ฆฌ์ค์ ์ฝ๋์ ์ง์นจ์ ์ด ์ฌ๋ก ์ฐ๊ตฌ ๋ด solution ํด๋์์ ์ฐพ์ ์ ์์ต๋๋ค. ๋ค์ ๋จ๊ณ์ ๋ฐ๋ผ ์ฐ๊ฒฐ์ ์ค์ ํ์ธ์:
microsoft_docs_search ๋๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์์ ํจ๊ป ํธ์ถํ์ฌ ๋ฌธ์ ๊ฐ์ ธ์ค๊ธฐ์ด ์๋๋ฆฌ์ค๋ ๋ค์์ ์์ฐํฉ๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
Prompt> What is Azure Key Vault?
Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
์๋๋ ์ต์ ์ํ ์๋ฃจ์ ์ ๋๋ค. ์ ์ฒด ์ฝ๋์ ์์ธํ ๋ด์ฉ์ solution ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ๋ฌธ์๋ฅผ ๊ฒ์ํ ์ ์๊ฒ ํ์ฌ, ์ฑ์ด๋ ์ฌ์ดํธ ๋ด์์ ์ฆ์ ๋ฌธ์ ์ ๊ทผ์ ๊ฐ๋ฅํ๊ฒ ํ๋ ๊ฒ์ ๋๋ค.
๋ค์ ๋ด์ฉ์ ๋ฐฐ์ธ ์ ์์ต๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
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 ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ์๋ฒ๋ฅผ ์๋ํฐ ๋ด์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ค์์ด ๊ฐ๋ฅํฉ๋๋ค:
๋ค์ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค:
.vscode/mcp.json ํ์ผ ์ถ๊ฐ(์์๋ ์๋ ์ฐธ์กฐ)VS Code์์ MCP ์๋ฒ ์ค์ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
{
"servers": {
"LearnDocsMCP": {
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
> ๋จ๊ณ๋ณ ๊ฐ์ด๋์ ์คํฌ๋ฆฐ์ท์ด ํฌํจ๋ ์์ธํ ์ค๋ช
์ README.md๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด ๋ฐฉ๋ฒ์ ๊ธฐ์ ๊ฐ์ข๋ฅผ ๋ง๋ค๊ฑฐ๋ ๋ฌธ์๋ฅผ ์์ฑํ๊ฑฐ๋ ๋น๋ฒํ๊ฒ ์ฐธ์กฐ๊ฐ ํ์ํ ์ฝ๋๋ฅผ ๊ฐ๋ฐํ๋ ๋ชจ๋ ์ฌ๋์๊ฒ ์ด์์ ์ ๋๋ค.
์ฃผ์ ์์
๋ฌธ์๋ฅผ ๋๊ตฌ์ ์ง์ ํตํฉํ๋ ๊ฒ์ ๋จ์ํ ํธ์๊ฐ ์๋๋ผ ์์ฐ์ฑ์ ํ๋ช ์ ๋๋ค. ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ฉด:
์ด ๊ธฐ์ ๋ค์ ํจ์จ์ ์ผ ๋ฟ ์๋๋ผ ์ฌ์ฉํ๊ธฐ ์ฆ๊ฑฐ์ด ์๋ฃจ์ ์ ๋ง๋๋ ๋ฐ ๋์์ ์ค ๊ฒ์ ๋๋ค.
์ถ๊ฐ ์๋ฃ
์ดํด๋ฅผ ๊น๊ฒ ํ๋ ค๋ฉด ๋ค์ ๊ณต์ ์์์ ํ์ํด ๋ณด์ธ์:
๋ค์ ๋จ๊ณ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ Chainlit์ Model Context Protocol(MCP)์ ์ฌ์ฉํด ์ฃผ์ ๋ณ ๋ง์ถคํ ํ์ต ๊ณํ์ ์์ฑํ๋ ๋ํํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค. ์ฌ์ฉ์๋ ์๋ฅผ ๋ค์ด "AI-900 ์ธ์ฆ" ๊ฐ์ ์ฃผ์ ์ 8์ฃผ ๊ฐ์ ํ์ต ๊ธฐ๊ฐ์ ์ง์ ํ๋ฉด ์ฑ์ ์ฃผ๋ณ ์ถ์ฒ ์ฝํ ์ธ ๋ฅผ ์ ๊ณตํฉ๋๋ค. Chainlit์ ๋ํํ ์ฑํ ์ธํฐํ์ด์ค๋ฅผ ๊ฐ๋ฅํ๊ฒ ํ์ฌ ๊ฒฝํ์ ํฅ๋ฏธ๋กญ๊ณ ์ ์์ ์ผ๋ก ๋ง๋ญ๋๋ค.
์ด ํ๋ก์ ํธ๋ ๋ํํ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋
ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
์ฌ๋ก ์ฐ๊ตฌ: ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๊ธฐ
์ฝ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๊ณ ํ ๋ ๋ฌธ์ ์ฌ์ดํธ, Stack Overflow, ์๋ง์ ๊ฒ์ ์์ง ํญ ์ฌ์ด๋ฅผ ๋ฐ์๊ฒ ์ค๊ฐ ๋ณธ ์ ์ด ์๋์?
ํน์ ๋ฌธ์ ์ ์ฉ์ผ๋ก ๋ ๋ฒ์งธ ๋ชจ๋ํฐ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ IDE์ ๋ธ๋ผ์ฐ์ ์ฌ์ด๋ฅผ ๊ณ์ํด์ Alt+Tab ํ๋ ๊ฒฝ์ฐ๋ ์์ ๊ฒ๋๋ค.
๋ฌธ์๋ฅผ ์ํฌํ๋ก์ฐ ์์์โ์ฑ, IDE ๋๋ ๋ง์ถค ๋๊ตฌ์ ํตํฉํด์โ๋ฐ๋ก ํ์ธํ ์ ์๋ค๋ฉด ํจ์ฌ ๋ซ์ง ์์๊น์?
์ด๋ฒ ์ฌ๋ก ์ฐ๊ตฌ์์๋ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ง์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.
๊ฐ์
ํ๋ ๊ฐ๋ฐ์ ๋จ์ง ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์๋๋ผ, ์ ์์ ์ ์ ํ ์ ๋ณด๋ฅผ ์ฐพ๋ ์ผ์ ๋๋ค.
๋ฌธ์๋ ์ด๋์๋ ์กด์ฌํ์ง๋ง, ๊ฐ์ฅ ํ์ํ ๋์ธ ๋๊ตฌ์ ์ํฌํ๋ก์ฐ ๋ด๋ถ์ ์๋ ๊ฒฝ์ฐ๋ ๋๋ญ ๋๋ค.
๋ฌธ์ ๊ฒ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ง์ ํตํฉํ๋ฉด ์๊ฐ์ ์ ์ฝํ๊ณ , ์ปจํ ์คํธ ์ ํ์ ์ค์ด๋ฉฐ ์์ฐ์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
์ด ์น์ ์์๋ ํด๋ผ์ด์ธํธ๊ฐ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ์ ์๋ดํฉ๋๋ค.
์ฐ๊ฒฐ์ ์ค์ ํ๊ณ , ์์ฒญ์ ๋ณด๋ด๋ฉฐ, ์คํธ๋ฆฌ๋ฐ ์๋ต์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์ค๋ช ํฉ๋๋ค. ์ด ์ ๊ทผ๋ฒ์ ์ํฌํ๋ก์ฐ๋ฅผ ๊ฐ์ํํ ๋ฟ ์๋๋ผ, ๋ ๋๋ํ๊ณ ๋์์ด ๋๋ ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ๊ฐ๋ฅ์ฑ์ ์ด์ด์ค๋๋ค.
ํ์ต ๋ชฉํ
์ ์ด ์์ ์ ํ ๊น์? ์ต๊ณ ์ ๊ฐ๋ฐ์ ๊ฒฝํ์ ๋ง์ฐฐ์ ์ ๊ฑฐํ๋ ๋ฐ ์์ต๋๋ค. ์ฝ๋ ํธ์ง๊ธฐ, ์ฑ๋ด ๋๋ ์น ์ฑ์ด Microsoft Learn์ ์ต์ ์ฝํ ์ธ ๋ฅผ ์ฌ์ฉํด ๋ฌธ์ ์ง๋ฌธ์ ์ฆ์ ๋ตํ ์ ์๋ค๊ณ ์์ํด ๋ณด์ธ์. ์ด ์ฅ์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
์ด ๊ธฐ์ ๋ค์ ํตํด ๋ฐ์ํ ๋ฟ๋ง ์๋๋ผ ์ง์ ์ผ๋ก ๋ํํ์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์์ด ๊ฐ๋ฅํ ๋๊ตฌ๋ฅผ ๋ง๋๋ ๋ฒ์ ๋ฐฐ์ฐ์ค ๊ฒ๋๋ค.
์๋๋ฆฌ์ค 1 - MCP๋ฅผ ์ด์ฉํ ์ค์๊ฐ ๋ฌธ์ ๊ฒ์
์ด ์๋๋ฆฌ์ค์์๋ ํด๋ผ์ด์ธํธ๋ฅผ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค. ์ฑ์ ๋ ๋์ง ์๊ณ ๋ ์ค์๊ฐ ๋งฅ๋ฝ ์ธ์ ๋ฌธ์์ ์ ๊ทผํ ์ ์์ต๋๋ค.
์ค์ต์ ์์ํด ๋ด
์๋ค. Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ microsoft_docs_search ๋๊ตฌ๋ฅผ ํธ์ถํ๊ณ , ์คํธ๋ฆฌ๋ฐ ์๋ต์ ์ฝ์์ ๊ธฐ๋กํ๋ ์ฑ์ ์์ฑํ๋ ๊ฒ์ด ๊ณผ์ ์
๋๋ค.
์ ์ด ๋ฐฉ๋ฒ์ธ๊ฐ์?
์ด๊ฒ์ด ์ฑ๋ด, IDE ํ์ฅ, ์น ๋์๋ณด๋์ ๊ฐ์ ๊ณ ๊ธ ํตํฉ ๊ธฐ๋ฅ์ ๊ตฌ์ถํ๋ ํ ๋๊ฐ ๋๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด ์๋๋ฆฌ์ค์ ์ฝ๋์ ์ง์นจ์ ์ด ์ฌ๋ก ์ฐ๊ตฌ ๋ด solution ํด๋์์ ์ฐพ์ ์ ์์ต๋๋ค. ๋ค์ ๋จ๊ณ์ ๋ฐ๋ผ ์ฐ๊ฒฐ์ ์ค์ ํ์ธ์:
microsoft_docs_search ๋๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์์ ํจ๊ป ํธ์ถํ์ฌ ๋ฌธ์ ๊ฐ์ ธ์ค๊ธฐ์ด ์๋๋ฆฌ์ค๋ ๋ค์์ ์์ฐํฉ๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
Prompt> What is Azure Key Vault?
Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
์๋๋ ์ต์ ์ํ ์๋ฃจ์ ์ ๋๋ค. ์ ์ฒด ์ฝ๋์ ์์ธํ ๋ด์ฉ์ solution ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ๋ฌธ์๋ฅผ ๊ฒ์ํ ์ ์๊ฒ ํ์ฌ, ์ฑ์ด๋ ์ฌ์ดํธ ๋ด์์ ์ฆ์ ๋ฌธ์ ์ ๊ทผ์ ๊ฐ๋ฅํ๊ฒ ํ๋ ๊ฒ์ ๋๋ค.
๋ค์ ๋ด์ฉ์ ๋ฐฐ์ธ ์ ์์ต๋๋ค:
์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
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 ํด๋์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
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 ์๋ฒ๋ฅผ ์๋ํฐ ๋ด์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ค์์ด ๊ฐ๋ฅํฉ๋๋ค:
๋ค์ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค:
.vscode/mcp.json ํ์ผ ์ถ๊ฐ(์์๋ ์๋ ์ฐธ์กฐ)VS Code์์ MCP ์๋ฒ ์ค์ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
{
"servers": {
"LearnDocsMCP": {
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
> ๋จ๊ณ๋ณ ๊ฐ์ด๋์ ์คํฌ๋ฆฐ์ท์ด ํฌํจ๋ ์์ธํ ์ค๋ช
์ README.md๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด ๋ฐฉ๋ฒ์ ๊ธฐ์ ๊ฐ์ข๋ฅผ ๋ง๋ค๊ฑฐ๋ ๋ฌธ์๋ฅผ ์์ฑํ๊ฑฐ๋ ๋น๋ฒํ๊ฒ ์ฐธ์กฐ๊ฐ ํ์ํ ์ฝ๋๋ฅผ ๊ฐ๋ฐํ๋ ๋ชจ๋ ์ฌ๋์๊ฒ ์ด์์ ์ ๋๋ค.
์ฃผ์ ์์
๋ฌธ์๋ฅผ ๋๊ตฌ์ ์ง์ ํตํฉํ๋ ๊ฒ์ ๋จ์ํ ํธ์๊ฐ ์๋๋ผ ์์ฐ์ฑ์ ํ๋ช ์ ๋๋ค. ํด๋ผ์ด์ธํธ์์ Microsoft Learn Docs MCP ์๋ฒ์ ์ฐ๊ฒฐํ๋ฉด:
์ด ๊ธฐ์ ๋ค์ ํจ์จ์ ์ผ ๋ฟ ์๋๋ผ ์ฌ์ฉํ๊ธฐ ์ฆ๊ฑฐ์ด ์๋ฃจ์ ์ ๋ง๋๋ ๋ฐ ๋์์ ์ค ๊ฒ์ ๋๋ค.
์ถ๊ฐ ์๋ฃ
์ดํด๋ฅผ ๊น๊ฒ ํ๋ ค๋ฉด ๋ค์ ๊ณต์ ์์์ ํ์ํด ๋ณด์ธ์:
๋ค์ ๋จ๊ณ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ ๋ฌธ์๋ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ MCP ์๋ฒ๋ฅผ ์ด์ฉํด Microsoft Learn Docs๋ฅผ VS Code ํ๊ฒฝ ์์ผ๋ก ์ง์ ๋ถ๋ฌ์ค๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋คโ๋ ์ด์ ๋ธ๋ผ์ฐ์ ํญ์ ์ ํํ์ง ์์๋ ๋ฉ๋๋ค! ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
๊ตฌํ์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค:
.vscode/mcp.json ์์ ๊ตฌ์ฑ์ด ์๋๋ฆฌ์ค๋ ์ฝ์ค ์ ์, ๋ฌธ์ ์์ฑ์, ๊ฐ๋ฐ์๊ฐ ๋ฌธ์, Copilot, ๊ฒ์ฆ ๋๊ตฌ๋ฅผ ์๋ํฐ ๋ด์์ ์์ ํ๋ฉฐ ์ง์ค๋ ฅ์ ์ ์งํ๋ ๋ฐ ์ด์์ ์ ๋๋ค.
6. APIM MCP ์๋ฒ ์์ฑ
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ Azure API Management(APIM)๋ฅผ ์ฌ์ฉํด MCP ์๋ฒ๋ฅผ ๋ง๋๋ ๋จ๊ณ๋ณ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋ด์ฉ์ ๋ค์์ ํฌํจํฉ๋๋ค:
์ด ์์๋ Azure ๊ธฐ๋ฅ์ ํ์ฉํด ๊ฒฌ๊ณ ํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๊ณ , AI ์์คํ ๊ณผ ๊ธฐ์ API ๊ฐ ํตํฉ์ ๊ฐํํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ ์ค๋๋ค.
7. GitHub MCP ๋ ์ง์คํธ๋ฆฌ โ ์์ด์ ํฑ ํตํฉ ๊ฐ์ํ
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ 2025๋ 9์์ ์ถ์๋ GitHub MCP ๋ ์ง์คํธ๋ฆฌ๊ฐ AI ์ํ๊ณ์ ์ค์ํ ๋ฌธ์ ์ธ ๋ถ์ฐ๋ MCP ์๋ฒ ๋ฐ๊ฒฌ ๋ฐ ๋ฐฐํฌ ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง ์ดํด๋ด ๋๋ค.
๊ฐ์
MCP ๋ ์ง์คํธ๋ฆฌ๋ ๋ฆฌํฌ์งํ ๋ฆฌ์ ๋ ์ง์คํธ๋ฆฌ์ ํฉ์ด์ง MCP ์๋ฒ์ ์ฆ๊ฐํ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค. ์ด์ ์๋ ํตํฉ์ด ๋๋ฆฌ๊ณ ์ค๋ฅ๊ฐ ์ฆ์์ต๋๋ค. ์ด๋ฌํ ์๋ฒ๋ค์ AI ์์ด์ ํธ๊ฐ API, ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ฌธ์ ์์ค ๋ฑ ์ธ๋ถ ์์คํ ๊ณผ ์ํธ์์ฉํ ์ ์๊ฒ ํฉ๋๋ค.
๋ฌธ์ ์ ์
์์ด์ ํฑ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌ์ถํ๋ ๊ฐ๋ฐ์๋ค์ด ๋ง์ฃผํ ๋ฌธ์ :
์๋ฃจ์ ์ํคํ ์ฒ
GitHub MCP ๋ ์ง์คํธ๋ฆฌ๋ ์ ๋ขฐํ ์ ์๋ MCP ์๋ฒ๋ฅผ ์ค์ ์ง์คํํ๋ฉฐ ์ฃผ์ ๊ธฐ๋ฅ์:
๋น์ฆ๋์ค ์ํฅ
์ด ๋ ์ง์คํธ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ ์ธก์ ๊ฐ๋ฅํ ๊ฐ์ ์ ๊ฐ์ ธ์์ต๋๋ค:
github-mcp-server)๋ฅผ ํตํ ์์ฐ์ฑ ํฅ์์ ๋ต์ ๊ฐ์น
์์ด์ ํธ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ ๋ฐ ์ฌํ ๊ฐ๋ฅํ ์ํฌํ๋ก์ฐ ์ ๋ฌธ๊ฐ๋ MCP ๋ ์ง์คํธ๋ฆฌ๋ฅผ ํตํด:
์ด ์ฌ๋ก ์ฐ๊ตฌ๋ MCP ๋ ์ง์คํธ๋ฆฌ๊ฐ ๋จ์ํ ๋๋ ํฐ๋ฆฌ๋ฅผ ๋์ด ํ์ฅ ๊ฐ๋ฅํ๊ณ ์ค์ ๋ชจ๋ธ ํตํฉ๊ณผ ์์ด์ ํธ ์์คํ ๋ฐฐํฌ๋ฅผ ์ํ ๊ธฐ๋ฐ ํ๋ซํผ์์ ๋ณด์ฌ์ค๋๋ค.
๊ฒฐ๋ก
์ด ์ผ๊ณฑ ๊ฐ์ง ์ข ํฉ ์ฌ๋ก ์ฐ๊ตฌ๋ Model Context Protocol์ ๋ฐ์ด๋ ๋ค์ฌ๋ค๋ฅํจ๊ณผ ๋ค์ํ ์ค์ ์๋๋ฆฌ์ค์์์ ํ์ฉ์ ์ ์ฆํฉ๋๋ค. ๋ณต์กํ ๋ค์ค ์์ด์ ํธ ์ฌํ ๊ณํ ์์คํ , ๊ธฐ์ API ๊ด๋ฆฌ, ๊ฐ์ํ๋ ๋ฌธ์ ์ํฌํ๋ก์ฐ์์ ํ์ ์ ์ธ GitHub MCP ๋ ์ง์คํธ๋ฆฌ์ ์ด๋ฅด๊ธฐ๊น์ง, ์ด ์ฌ๋ก๋ค์ MCP๊ฐ AI ์์คํ ์ ๋๊ตฌ, ๋ฐ์ดํฐ, ์๋น์ค์ ์ฐ๊ฒฐํ๋ ํ์คํ๋๊ณ ํ์ฅ ๊ฐ๋ฅํ ๋ฐฉ์์ ์ ๊ณตํ์ฌ ํ์ํ ๊ฐ์น๋ฅผ ๊ตฌํํจ์ ๋ณด์ฌ์ค๋๋ค.
์ฌ๋ก ์ฐ๊ตฌ๋ MCP ๊ตฌํ์ ์ฌ๋ฌ ์ฐจ์์ ํฌ๊ดํฉ๋๋ค:
์ด ๊ตฌํ์ ํ์ตํจ์ผ๋ก์จ ์ป๋ ํต์ฌ ํต์ฐฐ์:
์ด ์ฌ๋ก๋ค์ MCP๊ฐ ๋จ์ ์ด๋ก ์ ํ๋ ์์ํฌ๊ฐ ์๋, ๋ณต์กํ ๋น์ฆ๋์ค ๋ฌธ์ ์ ์ค์ง์ ์๋ฃจ์ ์ ์ ๊ณตํ๋ ์ฑ์ํ๊ณ ํ๋ก๋์ ์ค๋น๋ ํ๋กํ ์ฝ์์ ๋ณด์ฌ์ค๋๋ค. ๋จ์ ์๋ํ ๋๊ตฌ๋ ์ ๊ตํ ๋ค์ค ์์ด์ ํธ ์์คํ ์ด๋ , ์ฌ๊ธฐ ์ ์๋ ํจํด๊ณผ ์ ๊ทผ๋ฒ์ ์ฌ๋ฌ๋ถ ์์ ์ MCP ํ๋ก์ ํธ์ ๊ฒฌ๊ณ ํ ๊ธฐ๋ฐ์ ์ ๊ณตํฉ๋๋ค.
์ถ๊ฐ ์๋ฃ
๋ค์ ๋จ๊ณ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ๋ฌธ์์ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ๊ฐ์ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
Module 10 — AI ํดํท
AI ์ํฌํ๋ก์ฐ ๊ฐ์ํ: AI Toolkit์ผ๋ก MCP ์๋ฒ ๊ตฌ์ถ
๐ฏ ๊ฐ์
_(์ ์ด๋ฏธ์ง๋ฅผ ํด๋ฆญํ์ฌ ์ด ๊ฐ์์ ๋น๋์ค๋ฅผ ์์ฒญํ์ธ์)_
Model Context Protocol (MCP) ์ํฌ์์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค! ์ด ์ข ํฉ ์ค์ต ์ํฌ์์ AI ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ํ์ ํ๋ ๋ ๊ฐ์ง ์ต์ฒจ๋จ ๊ธฐ์ ์ ๊ฒฐํฉํฉ๋๋ค:
๐ ๋ฐฐ์ธ ๋ด์ฉ
์ด ์ํฌ์์ด ๋๋๋ฉด AI ๋ชจ๋ธ๊ณผ ์ค์ ๋๊ตฌ ๋ฐ ์๋น์ค๋ฅผ ์ฐ๊ฒฐํ๋ ์ง๋ฅํ ์ ํ๋ฆฌ์ผ์ด์ ๊ตฌ์ถ ๊ธฐ์ ์ ์ต๋ํ๊ฒ ๋ฉ๋๋ค. ์๋ํ๋ ํ ์คํธ๋ถํฐ ๋ง์ถคํ API ํตํฉ๊น์ง ๋ณต์กํ ๋น์ฆ๋์ค ๊ณผ์ ๋ฅผ ํด๊ฒฐํ๋ ์ค๋ฌด ๋ฅ๋ ฅ์ ๊ฐ์ถ๊ฒ ๋ฉ๋๋ค.
๐๏ธ ๊ธฐ์ ์คํ
๐ Model Context Protocol (MCP)
MCP๋ AI ๋ชจ๋ธ์ ์ธ๋ถ ๋๊ตฌ ๋ฐ ๋ฐ์ดํฐ ์์ค์ ์ฐ๊ฒฐํ๋ "AI์ฉ USB-C"์ ๊ฐ์ ๋ฒ์ฉ ํ์ค์ ๋๋ค.
โจ ์ฃผ์ ํน์ง:
๐ฏ MCP์ ์ค์์ฑ:
USB-C๊ฐ ์ผ์ด๋ธ ํผ๋์ ์์ค ๊ฒ์ฒ๋ผ, MCP๋ AI ํตํฉ์ ๋ณต์ก์ฑ์ ์ ๊ฑฐํฉ๋๋ค. ํ๋์ ํ๋กํ ์ฝ, ๋ฌดํํ ๊ฐ๋ฅ์ฑ.
๐ค Visual Studio Code์ฉ AI Toolkit (AITK)
Microsoft์ ์ฃผ๋ ฅ AI ๊ฐ๋ฐ ํ์ฅ์ผ๋ก, VS Code๋ฅผ AI ํ์ ํ๋ซํผ์ผ๋ก ํ๋ฐ๊ฟ์ํต๋๋ค.
๐ ์ฃผ์ ๊ธฐ๋ฅ:
๐ก ๊ฐ๋ฐ ์ด์ :
๐ ํ์ต ์ฌ์
๐ ๋ชจ๋ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
๐ ๋ชจ๋ 1: AI Toolkit ๊ธฐ์ด
๐ ํ์ต ๋ชฉํ
์ด ๋ชจ๋์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
๐ง AI Toolkit (AITK) ์๊ฐ
Visual Studio Code์ฉ AI Toolkit์ ๋ง์ดํฌ๋ก์ํํธ์ ๋ํ ํ์ฅ ๊ธฐ๋ฅ์ผ๋ก, VS Code๋ฅผ ์ข ํฉ์ ์ธ AI ๊ฐ๋ฐ ํ๊ฒฝ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค. AI ์ฐ๊ตฌ์ ์ค์ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ ๊ฐ์ ๊ฐ๊ทน์ ๋ฉ์ฐ๋ฉฐ, ๋ชจ๋ ์์ค์ ๊ฐ๋ฐ์๊ฐ ์์ฑํ AI๋ฅผ ์ฝ๊ฒ ํ์ฉํ ์ ์๋๋ก ๋์ต๋๋ค.
๐ ์ฃผ์ ๊ธฐ๋ฅ
๐ฏ AI Toolkit์ด ์ค์ํ ์ด์
๐ ๏ธ ์ฌ์ ์ค๋น ๋ฐ ์ค์
๐ฆ AI Toolkit ํ์ฅ ์ค์น
1๋จ๊ณ: ํ์ฅ ๋ง์ผํ๋ ์ด์ค ์ ์
1. Visual Studio Code ์คํ
2. ํ์ฅ ๋ทฐ ์ด๊ธฐ (Ctrl+Shift+X ๋๋ Cmd+Shift+X)
3. "AI Toolkit" ๊ฒ์
2๋จ๊ณ: ๋ฒ์ ์ ํ
3๋จ๊ณ: ์ค์น ๋ฐ ํ์ฑํ
โ ํ์ธ ์ฒดํฌ๋ฆฌ์คํธ
๐งช ์ค์ต 1: GitHub ๋ชจ๋ธ ํ์
๐ฏ ๋ชฉํ: ๋ชจ๋ธ ์นดํ๋ก๊ทธ๋ฅผ ์ตํ๊ณ ์ฒซ AI ๋ชจ๋ธ ํ ์คํธํ๊ธฐ
๐ 1๋จ๊ณ: ๋ชจ๋ธ ์นดํ๋ก๊ทธ ํ์
๋ชจ๋ธ ์นดํ๋ก๊ทธ๋ AI ์ํ๊ณ๋ก ๊ฐ๋ ๊ด๋ฌธ์ ๋๋ค. ์ฌ๋ฌ ์ ๊ณต์ ์ฒด์ ๋ชจ๋ธ์ ํ๋ฐ ๋ชจ์ ์ฝ๊ฒ ํ์ํ๊ณ ๋น๊ตํ ์ ์์ต๋๋ค.
๐ ํ์ ๊ฐ์ด๋:
AI Toolkit ์ฌ์ด๋๋ฐ์์ MODELS - Catalog ํด๋ฆญ
๐ก ํ: ์ฝ๋ ์์ฑ, ์ฐฝ์์ ๊ธ์ฐ๊ธฐ, ๋ถ์ ๋ฑ ์ฌ์ฉ ์ฌ๋ก์ ๋ง๋ ํน์ ๊ธฐ๋ฅ์ ๊ฐ์ง ๋ชจ๋ธ์ ์ฐพ์๋ณด์ธ์.
โ ๏ธ ์ฃผ์: GitHub์ ํธ์คํ ๋ ๋ชจ๋ธ(GitHub Models)์ ๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์์ง๋ง ์์ฒญ ๋ฐ ํ ํฐ์ ์ ํ์ด ์์ต๋๋ค. Azure AI๋ ๋ค๋ฅธ ์๋ํฌ์ธํธ๋ฅผ ํตํด ํธ์คํ ๋ ๋น-GitHub ๋ชจ๋ธ์ ์ ๊ทผํ๋ ค๋ฉด ์ ์ ํ API ํค๋ ์ธ์ฆ์ด ํ์ํฉ๋๋ค.
๐ 2๋จ๊ณ: ์ฒซ ๋ชจ๋ธ ์ถ๊ฐ ๋ฐ ์ค์
๋ชจ๋ธ ์ ํ ์ ๋ต:
๐ง ์ค์ ์ ์ฐจ:
1. ์นดํ๋ก๊ทธ์์ OpenAI GPT-4.1 ์ ํ
2. Add to My Models ํด๋ฆญํ์ฌ ๋ชจ๋ธ ๋ฑ๋ก
3. Try in Playground ์ ํํด ํ ์คํธ ํ๊ฒฝ ์คํ
4. ๋ชจ๋ธ ์ด๊ธฐํ ๋๊ธฐ (์ฒซ ์คํ ์ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์)
โ๏ธ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ ์ดํดํ๊ธฐ:
๐ฏ 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. ๊ฒฐ๊ณผ ์น์ ์ ๋ฐ๊ฒฌ ๋ด์ฉ ๊ธฐ๋ก
๐ก ์์์ผ ํ ํต์ฌ ์ธ์ฌ์ดํธ:
๐ค ์ค์ต 2: Agent Builder๋ก ๋ง์ถคํ ์์ด์ ํธ ๋ง๋ค๊ธฐ
๐ฏ ๋ชฉํ: ํน์ ์์ ๊ณผ ์ํฌํ๋ก์ฐ์ ๋ง์ถ ์ ๋ฌธ AI ์์ด์ ํธ ์์ฑ
๐๏ธ 1๋จ๊ณ: Agent Builder ์ดํดํ๊ธฐ
Agent Builder๋ AI Toolkit์ ํต์ฌ ๊ธฐ๋ฅ์ ๋๋ค. ๋ํ ์ธ์ด ๋ชจ๋ธ์ ํ์ ๋ง์ถคํ ์ง์, ํน์ ํ๋ผ๋ฏธํฐ, ์ ๋ฌธ ์ง์๊ณผ ๊ฒฐํฉํด ๋ชฉ์ ์ ๋ง๋ AI ๋น์๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
๐ง ์์ด์ ํธ ์ํคํ ์ฒ ๊ตฌ์ฑ์์:
โ๏ธ 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๊ฐ ํ๋กฌํํธ ์์ฑ๊ณผ ์ต์ ํ๋ฅผ ๋์์ค ์๋ ์์ต๋๋ค*
๐ง ํ๋ผ๋ฏธํฐ ์ต์ ํ:
๐ 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์ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ๋ง์คํฐํ์ผ๋ฉฐ, ๋ ๊ณ ๊ธ AI ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ค๋น๊ฐ ๋์์ต๋๋ค!
๐ ๋ค์ ๋ชจ๋๋ก ๊ณ์ํ๊ธฐ
๋ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด ๋ชจ๋ 2: MCP with AI Toolkit Fundamentals ๋ก ์ด๋ํ์ธ์. ์ฌ๊ธฐ์ ๋ค์์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค:
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์์ ์๊ฐ: 15๋ถ
๐ฏ ํ์ต ๋ชฉํ: 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ์ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
๐ ๋ชจ๋ 2: AI Toolkit๊ณผ ํจ๊ปํ๋ MCP ๊ธฐ๋ณธ ๊ฐ๋
๐ ํ์ต ๋ชฉํ
์ด ๋ชจ๋์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
๐ฏ ๋ชจ๋ 1์์ ์ด์ด์
๋ชจ๋ 1์์๋ AI Toolkit ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ๊ณ ์ฒซ Python ์์ด์ ํธ๋ฅผ ๋ง๋ค์์ต๋๋ค. ์ด์ ํ์ ์ ์ธ Model Context Protocol (MCP)์ ํตํด ์ธ๋ถ ๋๊ตฌ์ ์๋น์ค์ ์ฐ๊ฒฐํ์ฌ ์์ด์ ํธ๋ฅผ ๊ฐ๋ ฅํ๊ฒ ์ ๊ทธ๋ ์ด๋ํ ์ฐจ๋ก์ ๋๋ค.
๊ธฐ๋ณธ ๊ณ์ฐ๊ธฐ์์ ์์ ํ ์ปดํจํฐ๋ก ์ ๊ทธ๋ ์ด๋ํ๋ ๊ฒ๊ณผ ๊ฐ๋ค๊ณ ์๊ฐํ์ธ์ โ AI ์์ด์ ํธ๊ฐ ๋ค์๊ณผ ๊ฐ์ ๋ฅ๋ ฅ์ ๊ฐ์ถ๊ฒ ๋ฉ๋๋ค:
๐ง 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]
๐ง ํต์ฌ ๊ตฌ์ฑ ์์:
๐ข Microsoft์ MCP ์๋ฒ ์ํ๊ณ
Microsoft๋ ์ค์ ๋น์ฆ๋์ค ์๊ตฌ๋ฅผ ์ถฉ์กฑํ๋ ์ํฐํ๋ผ์ด์ฆ๊ธ ์๋ฒ ์ ํ๊ตฐ์ผ๋ก MCP ์ํ๊ณ๋ฅผ ์ ๋ํ๊ณ ์์ต๋๋ค.
๐ ์ฃผ์ Microsoft MCP ์๋ฒ
1. โ๏ธ Azure MCP ์๋ฒ
๐ ์ ์ฅ์: azure/azure-mcp
๐ฏ ๋ชฉ์ : AI ํตํฉ์ ํตํ ์ข ํฉ Azure ๋ฆฌ์์ค ๊ด๋ฆฌ
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
2. ๐ Microsoft Dataverse MCP
๐ ๋ฌธ์: Microsoft Dataverse Integration
๐ฏ ๋ชฉ์ : ๋น์ฆ๋์ค ๋ฐ์ดํฐ๋ฅผ ์ํ ์์ฐ์ด ์ธํฐํ์ด์ค
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
3. ๐ Playwright MCP ์๋ฒ
๐ ์ ์ฅ์: microsoft/playwright-mcp
๐ฏ ๋ชฉ์ : ๋ธ๋ผ์ฐ์ ์๋ํ ๋ฐ ์น ์ํธ์์ฉ ๊ธฐ๋ฅ ์ ๊ณต
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
4. ๐ Files MCP ์๋ฒ
๐ ์ ์ฅ์: microsoft/files-mcp-server
๐ฏ ๋ชฉ์ : ์ง๋ฅํ ํ์ผ ์์คํ ์์
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
5. ๐ MarkItDown MCP ์๋ฒ
๐ ์ ์ฅ์: microsoft/markitdown
๐ฏ ๋ชฉ์ : ๊ณ ๊ธ Markdown ์ฒ๋ฆฌ ๋ฐ ์กฐ์
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
6. ๐ Clarity MCP ์๋ฒ
๐ฆ ํจํค์ง: @microsoft/clarity-mcp-server
๐ฏ ๋ชฉ์ : ์น ๋ถ์ ๋ฐ ์ฌ์ฉ์ ํ๋ ์ธ์ฌ์ดํธ ์ ๊ณต
โจ ์ฃผ์ ๊ธฐ๋ฅ:
๐ ํ์ฉ ์ฌ๋ก:
๐ ์ปค๋ฎค๋ํฐ ์ํ๊ณ
Microsoft ์๋ฒ ์ธ์๋ MCP ์ํ๊ณ์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค:
๐ ๏ธ ์ค์ต: ๋ธ๋ผ์ฐ์ ์๋ํ ์์ด์ ํธ ๋ง๋ค๊ธฐ
๐ฏ ํ๋ก์ ํธ ๋ชฉํ: Playwright MCP ์๋ฒ๋ฅผ ์ฌ์ฉํด ์น์ฌ์ดํธ๋ฅผ ํ์ํ๊ณ ์ ๋ณด๋ฅผ ์ถ์ถํ๋ฉฐ ๋ณต์กํ ์น ์ํธ์์ฉ์ ์ํํ๋ ์ง๋ฅํ ๋ธ๋ผ์ฐ์ ์๋ํ ์์ด์ ํธ๋ฅผ ๋ง๋ญ๋๋ค.
๐ 1๋จ๊ณ: ์์ด์ ํธ ๊ธฐ๋ณธ ์ค์
1๋จ๊ณ: ์์ด์ ํธ ์ด๊ธฐํ
1. AI Toolkit Agent Builder ์ด๊ธฐ
2. ์ ์์ด์ ํธ ์์ฑ ๋ฐ ๋ค์ ์ค์ ์ ์ฉ:
- ์ด๋ฆ: BrowserAgent
- ๋ชจ๋ธ: GPT-4o ์ ํ
๐ง 2๋จ๊ณ: MCP ํตํฉ ์ํฌํ๋ก์ฐ
3๋จ๊ณ: MCP ์๋ฒ ํตํฉ ์ถ๊ฐ
1. Agent Builder์์ ๋๊ตฌ ์น์ ์ผ๋ก ์ด๋
2. "๋๊ตฌ ์ถ๊ฐ" ํด๋ฆญํ์ฌ ํตํฉ ๋ฉ๋ด ์ด๊ธฐ
3. "MCP ์๋ฒ" ์ ํ
๐ ๋๊ตฌ ์ ํ ์ดํดํ๊ธฐ:
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, reloadclick, fill, press, hover, dragtextContent, innerHTML, getAttributeisVisible, isEnabled, waitForSelectorscreenshot, pdf, videosetExtraHTTPHeaders, route, waitForResponse7๋จ๊ณ: ํตํฉ ์ฑ๊ณต ํ์ธ
โ ์ฑ๊ณต ์งํ:
๐ง ์ผ๋ฐ ๋ฌธ์ ํด๊ฒฐ:
๐ฏ 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 ํตํฉ์ ์ฑ๊ณต์ ์ผ๋ก ๋ง์คํฐํ์ฌ ์ธ๋ถ ๋๊ตฌ ๊ธฐ๋ฅ์ ๊ฐ์ถ ํ๋ก๋์ ์ค๋น AI ์์ด์ ํธ๋ฅผ ๋ง๋ค ์ ์๊ฒ ๋์์ต๋๋ค!
๐ ๋ค์ ๋ชจ๋๋ก ์งํ
MCP ๊ธฐ์ ์ ํ ๋จ๊ณ ๋ ๋ฐ์ ์ํค๊ณ ์ถ๋ค๋ฉด, ๋ชจ๋ 3: AI Toolkit๊ณผ ํจ๊ปํ๋ ๊ณ ๊ธ MCP ๊ฐ๋ฐ์ผ๋ก ์ด๋ํ์ธ์. ์ฌ๊ธฐ์ ๋ค์์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค:
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ์ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์์ ์๊ฐ: 20๋ถ
๐ฏ ํ์ต ๋ชฉํ: ์ธ๋ถ ๋๊ตฌ๊ฐ ๊ฐํ๋ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
๐ง ๋ชจ๋ 3: AI Toolkit์ ํ์ฉํ ๊ณ ๊ธ MCP ๊ฐ๋ฐ
๐ฏ ํ์ต ๋ชฉํ
์ด ์ค์ต์ ๋ง์น๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
๐ ์ฌ์ ์ค๋น ์ฌํญ
๐๏ธ ๋ง๋ค๊ฒ ๋ ๊ฒ
์ด๋ฒ ์ค์ต์์๋ Weather MCP Server๋ฅผ ๋ง๋ค์ด ๋ค์์ ๋ณด์ฌ์ค๋๋ค:
---
๐ง ํต์ฌ ๊ตฌ์ฑ ์์ ๊ฐ์
๐ 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. ์ธํฐ๋ํฐ๋ธ ํ ์คํธ ํ๊ฒฝ ํ์:
- ์ฌ์ฉ ๊ฐ๋ฅํ ๋๊ตฌ ํ์ธ
- ๋๊ตฌ ์คํ ํ ์คํธ
- ๋คํธ์ํฌ ์์ฒญ ๋ชจ๋ํฐ๋ง
- ์๋ฒ ์๋ต ๋๋ฒ๊น
---
๐ฏ ์ฃผ์ ํ์ต ์ฑ๊ณผ
์ด ์ค์ต์ ์๋ฃํ์ฌ ๋ค์์ ๋ฌ์ฑํ์ต๋๋ค:
๐ง ํ๊ตฌํ ๊ณ ๊ธ ๊ธฐ๋ฅ
๐ ์ถ๊ฐ ์๋ฃ
---
๐ ์ถํํฉ๋๋ค! Lab 3์ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃํ์ฌ ์ ๋ฌธ์ ์ธ ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ๋ก ๋ง์ถคํ MCP ์๋ฒ๋ฅผ ์์ฑ, ๋๋ฒ๊น , ๋ฐฐํฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
๐ ๋ค์ ๋ชจ๋๋ก ๊ณ์ ์งํ
์ค์ ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ์ MCP ๊ธฐ์ ์ ์ ์ฉํ ์ค๋น๊ฐ ๋์ จ๋์? ๋ชจ๋ 4: ์ค์ MCP ๊ฐ๋ฐ - ๋ง์ถคํ GitHub ํด๋ก ์๋ฒ๋ก ์ด๋ํ์ฌ:
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์์ ์๊ฐ: 20๋ถ
๐ฏ ํ์ต ๋ชฉํ: ์ต์ ๋๊ตฌ๋ก ์ปค์คํ 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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํฌ๋ ์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ ๊ฒฐ๊ณผ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ํดํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด๋ก ์์ฑ๋ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๋ํด์๋ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ์ ํฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.
๐ ๋ชจ๋ 4: ์ค์ฉ์ ์ธ MCP ๊ฐ๋ฐ - ๋ง์ถคํ GitHub ํด๋ก ์๋ฒ
> โก ๋น ๋ฅธ ์์: 30๋ถ ๋ง์ GitHub ์ ์ฅ์ ํด๋ก๋๊ณผ VS Code ํตํฉ์ ์๋ํํ๋ ์์ฐ ์ค๋น ์๋ฃ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ์ธ์!
๐ฏ ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ํ ์ ์์ต๋๋ค:
๐ ์ฌ์ ์ค๋น ์ฌํญ
๐๏ธ ํ๋ก์ ํธ ๊ฐ์
์ค์ ๊ฐ๋ฐ ๊ณผ์
๊ฐ๋ฐ์๋ก์ ์ฐ๋ฆฌ๋ ์์ฃผ GitHub ์ ์ฅ์๋ฅผ ํด๋ก ํ๊ณ VS Code ๋๋ VS Code Insiders์์ ์ฝ๋๋ค. ์ด ์๋ ๊ณผ์ ์ ๋ค์ ๋จ๊ณ๋ฅผ ํฌํจํฉ๋๋ค:
1. ํฐ๋ฏธ๋/๋ช ๋ น ํ๋กฌํํธ ์ด๊ธฐ
2. ์ํ๋ ๋๋ ํฐ๋ฆฌ๋ก ์ด๋
3. git clone ๋ช
๋ น ์คํ
4. ํด๋ก ํ ๋๋ ํฐ๋ฆฌ์์ VS Code ์ด๊ธฐ
์ฐ๋ฆฌ์ MCP ์๋ฃจ์ ์ ์ด ๊ณผ์ ์ ํ๋์ ์ค๋งํธํ ๋ช ๋ น์ผ๋ก ๊ฐ์ํํฉ๋๋ค!
๋ง๋ค๊ฒ ๋ ๊ฒ
GitHub Clone MCP ์๋ฒ (git_mcp_server)๋ ๋ค์์ ์ ๊ณตํฉ๋๋ค:
---
๐ ๋จ๊ณ๋ณ ๊ตฌํ
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"
์์ ๊ฒฐ๊ณผ:
4b. MCP Inspector์์ ํ ์คํธ
---
๐ ์ถํํฉ๋๋ค! ์ค์ ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ์ค์ฉ์ ์ด๊ณ ์์ฐ ์ค๋น ์๋ฃ๋ MCP ์๋ฒ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ง๋ค์์ต๋๋ค. ๋ง์ถค GitHub ํด๋ก ์๋ฒ๋ ๊ฐ๋ฐ์ ์์ฐ์ฑ์ ์๋ํํ๊ณ ํฅ์์ํค๊ธฐ ์ํ MCP์ ๊ฐ๋ ฅํจ์ ๋ณด์ฌ์ค๋๋ค.
๐ ๋ฌ์ฑํ ์ฑ๊ณผ:
---
๐ ์ํฌ์ ์๋ฃ: Model Context Protocol ์ฌ์
์ํฌ์ ์ฐธ๊ฐ์ ์ฌ๋ฌ๋ถ,
Model Context Protocol ์ํฌ์์ ๋ค ๊ฐ ๋ชจ๋ ๋ชจ๋๋ฅผ ์๋ฃํ์ ๊ฒ์ ์ถํ๋๋ฆฝ๋๋ค! ๊ธฐ๋ณธ AI Toolkit ๊ฐ๋ ์ดํด๋ถํฐ ์ค์ ๊ฐ๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ์์ฐ ์ค๋น ์๋ฃ MCP ์๋ฒ ๊ตฌ์ถ๊น์ง ๊ธด ์ฌ์ ์ ๊ฑธ์ด์ค์ จ์ต๋๋ค.
๐ ํ์ต ๊ฒฝ๋ก ์์ฝ:
๋ชจ๋ 1: AI Toolkit ๊ธฐ์ด, ๋ชจ๋ธ ํ ์คํธ, ์ฒซ ๋ฒ์งธ AI ์์ด์ ํธ ์์ฑ ํ์
๋ชจ๋ 2: MCP ์ํคํ ์ฒ ํ์ต, Playwright MCP ํตํฉ, ์ฒซ ๋ธ๋ผ์ฐ์ ์๋ํ ์์ด์ ํธ ๊ตฌ์ถ
๋ชจ๋ 3: Weather MCP ์๋ฒ์ ๋๋ฒ๊น ๋๊ตฌ๋ฅผ ํตํ ๋ง์ถค MCP ์๋ฒ ๊ฐ๋ฐ ๊ณ ๊ธ ๊ณผ์
๋ชจ๋ 4: ์ค์ฉ์ ์ธ GitHub ์ ์ฅ์ ์ํฌํ๋ก์ฐ ์๋ํ ๋๊ตฌ๋ฅผ ์ ์ ์ ์ฉ
๐ ๋ง์คํฐํ ๋ด์ฉ:
๐ฎ ์์ผ๋ก์ ๋จ๊ณ:
1. ์์ ๋ง์ MCP ์๋ฒ ๊ตฌ์ถ: ๊ณ ์ ํ ์ํฌํ๋ก์ฐ ์๋ํ์ ์ด ๊ธฐ์ ํ์ฉ
2. MCP ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: ์ฐฝ์๋ฌผ ๊ณต์ ๋ฐ ๋ฐฐ์ฐ๊ธฐ
3. ๊ณ ๊ธ ํตํฉ ํํ: MCP ์๋ฒ๋ฅผ ์ํฐํ๋ผ์ด์ฆ ์์คํ ๊ณผ ์ฐ๊ฒฐ
4. ์คํ ์์ค ๊ธฐ์ฌ: MCP ๋๊ตฌ์ ๋ฌธ์ ๊ฐ์ ์ ๊ธฐ์ฌ
์ด ์ํฌ์์ ์์์ ๋ถ๊ณผํฉ๋๋ค. Model Context Protocol ์ํ๊ณ๋ ๋น ๋ฅด๊ฒ ์งํ ์ค์ด๋ฉฐ, ์ฌ๋ฌ๋ถ์ AI ๊ธฐ๋ฐ ๊ฐ๋ฐ ๋๊ตฌ ์ ๋์ ์ค ์ค๋น๊ฐ ๋์ด ์์ต๋๋ค.
์ฐธ์ฌ์ ํ์ต์ ๊ฐ์ฌ๋๋ฆฝ๋๋ค!
์ด ์ํฌ์์ด ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์์ AI ๋๊ตฌ๋ฅผ ๊ตฌ์ถํ๊ณ ํ์ฉํ๋ ๋ฐฉ์์ ํ์ ํ๋ ์์ด๋์ด์ ๋ถ์จ๊ฐ ๋๊ธธ ๋ฐ๋๋๋ค.
์ฆ๊ฑฐ์ด ์ฝ๋ฉ ๋์ธ์!
---
๋ค์ ๋จ๊ณ
๋ชจ๋ 10 ๊ด๋ จ ๋ชจ๋ ์ค์ต์ ์๋ฃํ์ ๊ฒ์ ์ถํ๋๋ฆฝ๋๋ค!
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํฌ๋ ์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ ๊ฒฐ๊ณผ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ํดํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด๋ก ์์ฑ๋ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๋ํด์๋ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ์ ํฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์์ ์๊ฐ: 30๋ถ
๐ฏ ํ์ต ๋ชฉํ: ์ค์ ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ๋ฅผ ๊ฐ์ํํ๋ ์์ฐ ์ค๋น๋ MCP ์๋ฒ ๋ฐฐํฌ
๐ก ์ค์ ์ ์ฉ ์ฌ๋ก ๋ฐ ์ํฅ
๐ข ๊ธฐ์ ์ฉ ํ์ฉ ์ฌ๋ก
๐ DevOps ์๋ํ
์ง๋ฅํ ์๋ํ๋ฅผ ํตํด ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ ํ์ :
๐งช ํ์ง ๋ณด์ฆ ํ์
AI ๊ธฐ๋ฐ ์๋ํ๋ก ํ ์คํธ ์์ค ํฅ์:
๐ ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ ์ธํ ๋ฆฌ์ ์ค
๋ ์ค๋งํธํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ํฌํ๋ก์ฐ ๊ตฌ์ถ:
๐ง ๊ณ ๊ฐ ๊ฒฝํ ํฅ์
ํ์ํ ๊ณ ๊ฐ ์ํธ์์ฉ ์์ฑ:
๐ ๏ธ ์ฌ์ ์ค๋น ๋ฐ ์ค์
๐ป ์์คํ ์๊ตฌ์ฌํญ
๐ง ๊ฐ๋ฐ ํ๊ฒฝ
๊ถ์ฅ VS Code ํ์ฅ ํ๋ก๊ทธ๋จ
์ ํ ๋๊ตฌ
๐๏ธ ํ์ต ๊ฒฐ๊ณผ ๋ฐ ์ธ์ฆ ๊ฒฝ๋ก
๐ ์ญ๋ ๋ง์คํฐ ์ฒดํฌ๋ฆฌ์คํธ
์ด ์ํฌ์์ ์๋ฃํ๋ฉด ๋ค์์ ๋ง์คํฐ๋ฆฌ๋ฅผ ๋ฌ์ฑํ ์ ์์ต๋๋ค:
๐ฏ ํต์ฌ ์ญ๋
๐ง ๊ธฐ์ ์ ์ญ๋
๐ ๊ณ ๊ธ ์ญ๋
๐ ์ถ๊ฐ ์๋ฃ
---
๐ AI ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ ํ์ ํ ์ค๋น๊ฐ ๋์ จ๋์?
MCP์ AI Toolkit์ผ๋ก ํจ๊ป ์ง๋ฅํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฏธ๋๋ฅผ ๊ตฌ์ถํด ๋ด ์๋ค!
๋ค์ ๋จ๊ณ
๊ณ์ ์งํ: ๋ชจ๋ 11: MCP ์๋ฒ ์ค์ต
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ด์ฉ์ด ํฌํจ๋ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ฌธ์ ํด๋น ์ธ์ด์ ์๋ณธ ๋ฌธ์๊ฐ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ์ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
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 ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ํ์ต ๊ฒฝ๋ก
๐ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail ์์ ํ์ต ๊ตฌ์กฐ
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 ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๋ฅด์นฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐งญ ๋์ ๊ณผ์ : AI์ ์ค์ ๋ฐ์ดํฐ์ ๋ง๋จ
๊ธฐ์กด AI์ ํ๊ณ
ํ๋์ AI ์ด์์คํดํธ๋ ๋งค์ฐ ๊ฐ๋ ฅํ์ง๋ง ์ค์ ๋น์ฆ๋์ค ๋ฐ์ดํฐ์ ์์ ํ ๋ ์ค์ํ ํ๊ณ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค:
MCP ์๋ฃจ์
Model Context Protocol์ ๋ค์์ ํตํด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค:
๐ช Zava Retail ์๊ฐ: ํ์ต ์ฌ๋ก ์ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail
์ด ํ์ต ๊ฒฝ๋ก์์๋ Zava Retail์ด๋ผ๋ ๊ฐ์์ DIY ์๋งค ์ฒด์ธ์ ์ํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํฉ๋๋ค. ์ด ํ์ค์ ์ธ ์๋๋ฆฌ์ค๋ ์ํฐํ๋ผ์ด์ฆ๊ธ MCP ๊ตฌํ์ ๋ณด์ฌ์ค๋๋ค.
๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ
Zava Retail์ ๋ค์์ ์ด์ํฉ๋๋ค:
๋น์ฆ๋์ค ์๊ตฌ ์ฌํญ
๋งค์ฅ ๊ด๋ฆฌ์์ ์์์ AI ๊ธฐ๋ฐ ๋ถ์์ ํตํด ๋ค์์ ์ํํด์ผ ํฉ๋๋ค:
1. ๋งค์ฅ ๋ฐ ๊ธฐ๊ฐ๋ณ ํ๋งค ์ฑ๊ณผ ๋ถ์
2. ์ฌ๊ณ ์์ค ์ถ์ ๋ฐ ์ฌ์ ๊ณ ํ์์ฑ ์๋ณ
3. ๊ณ ๊ฐ ํ๋ ๋ฐ ๊ตฌ๋งค ํจํด ์ดํด
4. ์๋ฏธ ๊ฒ์์ ํตํ ์ ํ ํต์ฐฐ๋ ฅ ๋ฐ๊ฒฌ
5. ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋ณด๊ณ ์ ์์ฑ
6. ์ญํ ๊ธฐ๋ฐ ์ก์ธ์ค ์ ์ด๋ฅผ ํตํ ๋ฐ์ดํฐ ๋ณด์ ์ ์ง
๊ธฐ์ ์๊ตฌ ์ฌํญ
MCP ์๋ฒ๋ ๋ค์์ ์ ๊ณตํด์ผ ํฉ๋๋ค:
๐๏ธ 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 ์๋ฒ ๊ณ์ธต
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ๊ณ์ธต
3. ๋ณด์ ๊ณ์ธต
4. AI ๊ฐํ ๊ณ์ธต
๐ง ๊ธฐ์ ์คํ
ํต์ฌ ๊ธฐ์
๊ฐ๋ฐ ๋๊ตฌ
ํ๋ก๋์ ์คํ
๐ฌ ์ค์ ์ฌ์ฉ ์๋๋ฆฌ์ค
๋ค์ํ ์ฌ์ฉ์๊ฐ 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 ์ฐ๊ฒฐ์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค:
๋ฐ์ดํฐ ๋ณดํธ
๋ค์ค ๋ณด์ ๊ณ์ธต:
๐ฏ ์ฃผ์ ์์
์ด ์๊ฐ๋ฅผ ์๋ฃํ ํ ๋ค์์ ์ดํดํด์ผ ํฉ๋๋ค:
โ MCP ๊ฐ์น ์ ์: MCP๊ฐ AI ์ด์์คํดํธ์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ
โ ๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ: Zava Retail์ ์๊ตฌ ์ฌํญ๊ณผ ๊ณผ์
โ ์ํคํ ์ฒ ๊ฐ์: ์ฃผ์ ๊ตฌ์ฑ ์์์ ์ํธ์์ฉ
โ ๊ธฐ์ ์คํ: ์ฌ์ฉ๋ ๋๊ตฌ์ ํ๋ ์์ํฌ
โ ๋ณด์ ๋ชจ๋ธ: ๋ฉํฐ ํ ๋ํธ ๋ฐ์ดํฐ ์ก์ธ์ค ๋ฐ ๋ณดํธ
โ ์ฌ์ฉ ํจํด: ์ค์ ์ฟผ๋ฆฌ ์๋๋ฆฌ์ค์ ์ํฌํ๋ก
๐ ๋ค์ ๋จ๊ณ
๋ ๊น์ด ํ๊ตฌํ ์ค๋น๊ฐ ๋์ จ๋์? ๋ค์์ ์งํํ์ธ์:
Lab 01: ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
MCP ์๋ฒ ์ํคํ ์ฒ ํจํด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น, ์๋งค ๋ถ์ ์๋ฃจ์ ์ ์ง์ํ๋ ์์ธ ๊ธฐ์ ๊ตฌํ์ ๋ํด ์์๋ณด์ธ์.
๐ ์ถ๊ฐ ์๋ฃ
MCP ๋ฌธ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
Azure ์๋น์ค
---
๋ฉด์ฑ ์กฐํญ: ์ด๋ ๊ฐ์์ ์๋งค ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ํ์ต ์ฐ์ต์ ๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์ ์ ์ฌํ ์๋ฃจ์ ์ ๊ตฌํํ ๋๋ ํญ์ ์กฐ์ง์ ๋ฐ์ดํฐ ๊ฑฐ๋ฒ๋์ค ๋ฐ ๋ณด์ ์ ์ฑ ์ ๋ฐ๋ฅด์ญ์์ค.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
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 ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๋ฅด์นฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐งญ ๋์ ๊ณผ์ : AI์ ์ค์ ๋ฐ์ดํฐ์ ๋ง๋จ
๊ธฐ์กด AI์ ํ๊ณ
ํ๋์ AI ์ด์์คํดํธ๋ ๋งค์ฐ ๊ฐ๋ ฅํ์ง๋ง ์ค์ ๋น์ฆ๋์ค ๋ฐ์ดํฐ์ ์์ ํ ๋ ์ค์ํ ํ๊ณ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค:
MCP ์๋ฃจ์
Model Context Protocol์ ๋ค์์ ํตํด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค:
๐ช Zava Retail ์๊ฐ: ํ์ต ์ฌ๋ก ์ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail
์ด ํ์ต ๊ฒฝ๋ก์์๋ Zava Retail์ด๋ผ๋ ๊ฐ์์ DIY ์๋งค ์ฒด์ธ์ ์ํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํฉ๋๋ค. ์ด ํ์ค์ ์ธ ์๋๋ฆฌ์ค๋ ์ํฐํ๋ผ์ด์ฆ๊ธ MCP ๊ตฌํ์ ๋ณด์ฌ์ค๋๋ค.
๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ
Zava Retail์ ๋ค์์ ์ด์ํฉ๋๋ค:
๋น์ฆ๋์ค ์๊ตฌ ์ฌํญ
๋งค์ฅ ๊ด๋ฆฌ์์ ์์์ AI ๊ธฐ๋ฐ ๋ถ์์ ํตํด ๋ค์์ ์ํํด์ผ ํฉ๋๋ค:
1. ๋งค์ฅ ๋ฐ ๊ธฐ๊ฐ๋ณ ํ๋งค ์ฑ๊ณผ ๋ถ์
2. ์ฌ๊ณ ์์ค ์ถ์ ๋ฐ ์ฌ์ ๊ณ ํ์์ฑ ์๋ณ
3. ๊ณ ๊ฐ ํ๋ ๋ฐ ๊ตฌ๋งค ํจํด ์ดํด
4. ์๋ฏธ ๊ฒ์์ ํตํ ์ ํ ํต์ฐฐ๋ ฅ ๋ฐ๊ฒฌ
5. ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋ณด๊ณ ์ ์์ฑ
6. ์ญํ ๊ธฐ๋ฐ ์ก์ธ์ค ์ ์ด๋ฅผ ํตํ ๋ฐ์ดํฐ ๋ณด์ ์ ์ง
๊ธฐ์ ์๊ตฌ ์ฌํญ
MCP ์๋ฒ๋ ๋ค์์ ์ ๊ณตํด์ผ ํฉ๋๋ค:
๐๏ธ 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 ์๋ฒ ๊ณ์ธต
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ๊ณ์ธต
3. ๋ณด์ ๊ณ์ธต
4. AI ๊ฐํ ๊ณ์ธต
๐ง ๊ธฐ์ ์คํ
ํต์ฌ ๊ธฐ์
๊ฐ๋ฐ ๋๊ตฌ
ํ๋ก๋์ ์คํ
๐ฌ ์ค์ ์ฌ์ฉ ์๋๋ฆฌ์ค
๋ค์ํ ์ฌ์ฉ์๊ฐ 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 ์ฐ๊ฒฐ์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค:
๋ฐ์ดํฐ ๋ณดํธ
๋ค์ค ๋ณด์ ๊ณ์ธต:
๐ฏ ์ฃผ์ ์์
์ด ์๊ฐ๋ฅผ ์๋ฃํ ํ ๋ค์์ ์ดํดํด์ผ ํฉ๋๋ค:
โ MCP ๊ฐ์น ์ ์: MCP๊ฐ AI ์ด์์คํดํธ์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ
โ ๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ: Zava Retail์ ์๊ตฌ ์ฌํญ๊ณผ ๊ณผ์
โ ์ํคํ ์ฒ ๊ฐ์: ์ฃผ์ ๊ตฌ์ฑ ์์์ ์ํธ์์ฉ
โ ๊ธฐ์ ์คํ: ์ฌ์ฉ๋ ๋๊ตฌ์ ํ๋ ์์ํฌ
โ ๋ณด์ ๋ชจ๋ธ: ๋ฉํฐ ํ ๋ํธ ๋ฐ์ดํฐ ์ก์ธ์ค ๋ฐ ๋ณดํธ
โ ์ฌ์ฉ ํจํด: ์ค์ ์ฟผ๋ฆฌ ์๋๋ฆฌ์ค์ ์ํฌํ๋ก
๐ ๋ค์ ๋จ๊ณ
๋ ๊น์ด ํ๊ตฌํ ์ค๋น๊ฐ ๋์ จ๋์? ๋ค์์ ์งํํ์ธ์:
Lab 01: ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
MCP ์๋ฒ ์ํคํ ์ฒ ํจํด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น, ์๋งค ๋ถ์ ์๋ฃจ์ ์ ์ง์ํ๋ ์์ธ ๊ธฐ์ ๊ตฌํ์ ๋ํด ์์๋ณด์ธ์.
๐ ์ถ๊ฐ ์๋ฃ
MCP ๋ฌธ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
Azure ์๋น์ค
---
๋ฉด์ฑ ์กฐํญ: ์ด๋ ๊ฐ์์ ์๋งค ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ํ์ต ์ฐ์ต์ ๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์ ์ ์ฌํ ์๋ฃจ์ ์ ๊ตฌํํ ๋๋ ํญ์ ์กฐ์ง์ ๋ฐ์ดํฐ ๊ฑฐ๋ฒ๋์ค ๋ฐ ๋ณด์ ์ ์ฑ ์ ๋ฐ๋ฅด์ญ์์ค.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ ์ํคํ ์ฒ ํจํด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น, ๊ทธ๋ฆฌ๊ณ ๊ฒฌ๊ณ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ AI ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌํํ๋ ๊ธฐ์ ์ ์ ๋ต์ ๋ํ ์ฌ์ธต์ ์ธ ํ๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ํฌํจํ ํ๋ก๋์ ์ค๋น MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ค๋ฉด ์ ์คํ ์ํคํ ์ฒ ๊ฒฐ์ ์ด ํ์ํฉ๋๋ค. ์ด ์ค์ต์์๋ Zava Retail ๋ถ์ ์๋ฃจ์ ์ ๊ฒฌ๊ณ ํ๊ณ ์์ ํ๋ฉฐ ํ์ฅ ๊ฐ๋ฅํ๊ฒ ๋ง๋๋ ํต์ฌ ๊ตฌ์ฑ ์์, ์ค๊ณ ํจํด, ๊ธฐ์ ์ ๊ณ ๋ ค ์ฌํญ์ ๋ถํดํ์ฌ ์ค๋ช ํฉ๋๋ค.
๊ฐ ๊ณ์ธต์ด ์ด๋ป๊ฒ ์ํธ์์ฉํ๋์ง, ํน์ ๊ธฐ์ ์ด ์ ์ ํ๋์๋์ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ฌํ ํจํด์ ์์ ์ 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)
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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)
}
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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")
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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'
);
์ค๊ณ ์์น:
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์ ์ด์ :
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: ๋ณด์ ๋ฐ ๋ค์ค ํ ๋ํธ๋ก ๊ณ์ ์งํํ์ฌ ๋ค์์ ์ฌ์ธต์ ์ผ๋ก ํ๊ตฌํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
์ํคํ ์ฒ ํจํด
PostgreSQL ๊ณ ๊ธ ์ฃผ์
Python ๋น๋๊ธฐ ํจํด
---
๋ค์: ๋ณด์ ํจํด์ ํ๊ตฌํ ์ค๋น๊ฐ ๋์ จ๋์? Lab 02: ๋ณด์ ๋ฐ ๋ค์ค ํ ๋ํธ๋ก ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ ์ํคํ ์ฒ ํจํด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น, ๊ทธ๋ฆฌ๊ณ ๊ฒฌ๊ณ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ AI ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌํํ๋ ๊ธฐ์ ์ ์ ๋ต์ ๋ํ ์ฌ์ธต์ ์ธ ํ๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ํฌํจํ ํ๋ก๋์ ์ค๋น MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ค๋ฉด ์ ์คํ ์ํคํ ์ฒ ๊ฒฐ์ ์ด ํ์ํฉ๋๋ค. ์ด ์ค์ต์์๋ Zava Retail ๋ถ์ ์๋ฃจ์ ์ ๊ฒฌ๊ณ ํ๊ณ ์์ ํ๋ฉฐ ํ์ฅ ๊ฐ๋ฅํ๊ฒ ๋ง๋๋ ํต์ฌ ๊ตฌ์ฑ ์์, ์ค๊ณ ํจํด, ๊ธฐ์ ์ ๊ณ ๋ ค ์ฌํญ์ ๋ถํดํ์ฌ ์ค๋ช ํฉ๋๋ค.
๊ฐ ๊ณ์ธต์ด ์ด๋ป๊ฒ ์ํธ์์ฉํ๋์ง, ํน์ ๊ธฐ์ ์ด ์ ์ ํ๋์๋์ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ฌํ ํจํด์ ์์ ์ 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)
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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)
}
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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")
์ฃผ์ ๊ธฐ๋ฅ:
๊ณ์ธต 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'
);
์ค๊ณ ์์น:
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์ ์ด์ :
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: ๋ณด์ ๋ฐ ๋ค์ค ํ ๋ํธ๋ก ๊ณ์ ์งํํ์ฌ ๋ค์์ ์ฌ์ธต์ ์ผ๋ก ํ๊ตฌํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
์ํคํ ์ฒ ํจํด
PostgreSQL ๊ณ ๊ธ ์ฃผ์
Python ๋น๋๊ธฐ ํจํด
---
๋ค์: ๋ณด์ ํจํด์ ํ๊ตฌํ ์ค๋น๊ฐ ๋์ จ๋์? Lab 02: ๋ณด์ ๋ฐ ๋ค์ค ํ ๋ํธ๋ก ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์์ ์ ์ํ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ณด์ ๋ฐ ๋ฉํฐ ํ ๋์
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ์ ๋ํ ์ํฐํ๋ผ์ด์ฆ๊ธ ๋ณด์ ๋ฐ ๋ฉํฐ ํ ๋์ ๊ตฌํ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. ๋ฏผ๊ฐํ ์๋งค ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๋ฉด์ ์ฌ๋ฌ ํ ๋ํธ ๊ฐ์ ์ ์ฐํ ์ ๊ทผ ํจํด์ ๊ฐ๋ฅํ๊ฒ ํ๋ ์์ ํ๊ณ ์ค์ํ ์์คํ ์ ์ค๊ณํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
๊ณ ๊ฐ ๋ฐ์ดํฐ, ๊ฒฐ์ ์ ๋ณด, ๋น์ฆ๋์ค ์ธํ ๋ฆฌ์ ์ค๋ฅผ ์ฒ๋ฆฌํ๋ ์๋งค ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ณด์์ ๋งค์ฐ ์ค์ํฉ๋๋ค. ์ด ์ค์ต์์๋ ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ๋ถํฐ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ ๋ฐ ์ค์ ๋ชจ๋ํฐ๋ง๊น์ง ์์ ํ ๋ณด์ ์ํคํ ์ฒ๋ฅผ ๋ค๋ฃน๋๋ค.
Azure ID ์๋น์ค, PostgreSQL ํ ์์ค ๋ณด์, ์ ํ๋ฆฌ์ผ์ด์ ์์ค ์ ์ด, ํฌ๊ด์ ์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ฒฐํฉํ ์ฌ์ธต ๋ฐฉ์ด ์ ๋ต์ ๊ตฌํํ์ฌ ๊ฐ๋ ฅํ๊ณ ์ค์ํ ํ๋ซํผ์ ๋ง๋ญ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ๋ฉํฐ ํ ๋ํธ ๋ณด์ ์ํคํ ์ฒ
๋ณด์ ๊ณ์ธต ๊ฐ์
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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 ๊ธฐ์ด
-- 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 ๋ณด์
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณด์
๋ณด์ ํ ์คํธ
---
์ด์ : Lab 01: ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
๋ค์: Lab 03: ํ๊ฒฝ ์ค์
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์๋ฅผ ํด๋น ์ธ์ด๋ก ์์ฑ๋ ์ํ์์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ณด์ ๋ฐ ๋ฉํฐ ํ ๋์
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ์ ๋ํ ์ํฐํ๋ผ์ด์ฆ๊ธ ๋ณด์ ๋ฐ ๋ฉํฐ ํ ๋์ ๊ตฌํ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. ๋ฏผ๊ฐํ ์๋งค ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๋ฉด์ ์ฌ๋ฌ ํ ๋ํธ ๊ฐ์ ์ ์ฐํ ์ ๊ทผ ํจํด์ ๊ฐ๋ฅํ๊ฒ ํ๋ ์์ ํ๊ณ ์ค์ํ ์์คํ ์ ์ค๊ณํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
๊ณ ๊ฐ ๋ฐ์ดํฐ, ๊ฒฐ์ ์ ๋ณด, ๋น์ฆ๋์ค ์ธํ ๋ฆฌ์ ์ค๋ฅผ ์ฒ๋ฆฌํ๋ ์๋งค ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ณด์์ ๋งค์ฐ ์ค์ํฉ๋๋ค. ์ด ์ค์ต์์๋ ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ๋ถํฐ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ ๋ฐ ์ค์ ๋ชจ๋ํฐ๋ง๊น์ง ์์ ํ ๋ณด์ ์ํคํ ์ฒ๋ฅผ ๋ค๋ฃน๋๋ค.
Azure ID ์๋น์ค, PostgreSQL ํ ์์ค ๋ณด์, ์ ํ๋ฆฌ์ผ์ด์ ์์ค ์ ์ด, ํฌ๊ด์ ์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ฒฐํฉํ ์ฌ์ธต ๋ฐฉ์ด ์ ๋ต์ ๊ตฌํํ์ฌ ๊ฐ๋ ฅํ๊ณ ์ค์ํ ํ๋ซํผ์ ๋ง๋ญ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ๋ฉํฐ ํ ๋ํธ ๋ณด์ ์ํคํ ์ฒ
๋ณด์ ๊ณ์ธต ๊ฐ์
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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 ๊ธฐ์ด
-- 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 ๋ณด์
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณด์
๋ณด์ ํ ์คํธ
---
์ด์ : Lab 01: ํต์ฌ ์ํคํ ์ฒ ๊ฐ๋
๋ค์: Lab 03: ํ๊ฒฝ ์ค์
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์๋ฅผ ํด๋น ์ธ์ด๋ก ์์ฑ๋ ์ํ์์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํ๊ฒฝ ์ค์
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ PostgreSQL ํตํฉ์ ํตํด MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํ ์์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ค์ ํ๋ ๊ณผ์ ์ ์๋ดํฉ๋๋ค. ํ์ํ ๋ชจ๋ ๋๊ตฌ๋ฅผ ๊ตฌ์ฑํ๊ณ , Azure ๋ฆฌ์์ค๋ฅผ ๋ฐฐํฌํ๋ฉฐ, ๊ตฌํ์ ์งํํ๊ธฐ ์ ์ ์ค์ ์ ๊ฒ์ฆํฉ๋๋ค.
๊ฐ์
์ ์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ MCP ์๋ฒ ๊ฐ๋ฐ์ ์ฑ๊ณต์ ํ์์ ์ ๋๋ค. ์ด ์ค์ต์ Docker, Azure ์๋น์ค, ๊ฐ๋ฐ ๋๊ตฌ๋ฅผ ์ค์ ํ๊ณ ๋ชจ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ํ์ธํ๋ ๋จ๊ณ๋ณ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค.
์ด ์ค์ต์ ์๋ฃํ๋ฉด Zava Retail MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ ์ค๋น๊ฐ ๋ ์์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ๊ฐ์ถ๊ฒ ๋ฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ์ฌ์ ์๊ตฌ ์ฌํญ ํ์ธ
์์ํ๊ธฐ ์ ์ ๋ค์์ ํ์ธํ์ธ์:
ํ์ํ ์ง์
์์คํ ์๊ตฌ ์ฌํญ
๊ณ์ ์๊ตฌ ์ฌํญ
๐ ๏ธ ๋๊ตฌ ์ค์น
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 ๋ฆฌ์์ค๊ฐ ํ์ํฉ๋๋ค:
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. ์๋ ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
โ ๊ธฐ๋ณธ ๋๊ตฌ
โ Azure ๋ฆฌ์์ค
โ ํ๊ฒฝ ๊ตฌ์ฑ
.env ํ์ผ ์์ฑ ๋ฐ ๋ชจ๋ ํ์ ๋ณ์ ํฌํจaz account show๋ก ํ
์คํธ)โ VS Code ํตํฉ
.vscode/mcp.json ๊ตฌ์ฑ ์๋ฃ๐ ๏ธ ์ผ๋ฐ์ ์ธ ๋ฌธ์ ํด๊ฒฐ
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: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง๋ก ๊ณ์ ์งํํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
๊ฐ๋ฐ ๋๊ตฌ
Azure ์๋น์ค
Python ๊ฐ๋ฐ
---
๋ค์: ํ๊ฒฝ์ด ์ค๋น๋์๋์? Lab 04: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง๋ก ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํ๊ฒฝ ์ค์
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ PostgreSQL ํตํฉ์ ํตํด MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํ ์์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ค์ ํ๋ ๊ณผ์ ์ ์๋ดํฉ๋๋ค. ํ์ํ ๋ชจ๋ ๋๊ตฌ๋ฅผ ๊ตฌ์ฑํ๊ณ , Azure ๋ฆฌ์์ค๋ฅผ ๋ฐฐํฌํ๋ฉฐ, ๊ตฌํ์ ์งํํ๊ธฐ ์ ์ ์ค์ ์ ๊ฒ์ฆํฉ๋๋ค.
๊ฐ์
์ ์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ MCP ์๋ฒ ๊ฐ๋ฐ์ ์ฑ๊ณต์ ํ์์ ์ ๋๋ค. ์ด ์ค์ต์ Docker, Azure ์๋น์ค, ๊ฐ๋ฐ ๋๊ตฌ๋ฅผ ์ค์ ํ๊ณ ๋ชจ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ํ์ธํ๋ ๋จ๊ณ๋ณ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค.
์ด ์ค์ต์ ์๋ฃํ๋ฉด Zava Retail MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ ์ค๋น๊ฐ ๋ ์์ ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ๊ฐ์ถ๊ฒ ๋ฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ์ฌ์ ์๊ตฌ ์ฌํญ ํ์ธ
์์ํ๊ธฐ ์ ์ ๋ค์์ ํ์ธํ์ธ์:
ํ์ํ ์ง์
์์คํ ์๊ตฌ ์ฌํญ
๊ณ์ ์๊ตฌ ์ฌํญ
๐ ๏ธ ๋๊ตฌ ์ค์น
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 ๋ฆฌ์์ค๊ฐ ํ์ํฉ๋๋ค:
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. ์๋ ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
โ ๊ธฐ๋ณธ ๋๊ตฌ
โ Azure ๋ฆฌ์์ค
โ ํ๊ฒฝ ๊ตฌ์ฑ
.env ํ์ผ ์์ฑ ๋ฐ ๋ชจ๋ ํ์ ๋ณ์ ํฌํจaz account show๋ก ํ
์คํธ)โ VS Code ํตํฉ
.vscode/mcp.json ๊ตฌ์ฑ ์๋ฃ๐ ๏ธ ์ผ๋ฐ์ ์ธ ๋ฌธ์ ํด๊ฒฐ
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: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง๋ก ๊ณ์ ์งํํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
๊ฐ๋ฐ ๋๊ตฌ
Azure ์๋น์ค
Python ๊ฐ๋ฐ
---
๋ค์: ํ๊ฒฝ์ด ์ค๋น๋์๋์? Lab 04: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง๋ก ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์์๋ Zava Retail ์์คํ ์ ์ํ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ์ ๋ํด ๊น์ด ํ๊ตฌํฉ๋๋ค. ๋ฒกํฐ ๊ฒ์ ๊ธฐ๋ฅ, ๋ฉํฐ ํ ๋ํธ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง, ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ๋ฅผ ์ํ Row Level Security(RLS)๋ฅผ ํฌํจํ ํฌ๊ด์ ์ธ ์๋งค ์คํค๋ง๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ MCP ์๋ฒ์ ๊ธฐ๋ฐ์ผ๋ก, ์ฌ๋ฌ ๋งค์ฅ์ ์๋งค ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด์ ์๊ฒฉํ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ๋ฅผ ์ ์งํฉ๋๋ค. ์ฐ๋ฆฌ๋ 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 ์๋ฒ ๊ตฌํ์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
PostgreSQL & pgvector
๋ฉํฐ ํ ๋ํธ ์ํคํ ์ฒ
๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค
---
์ด์ : Lab 03: ํ๊ฒฝ ์ค์
๋ค์: Lab 05: MCP ์๋ฒ ๊ตฌํ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์คํค๋ง
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์์๋ Zava Retail ์์คํ ์ ์ํ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ์ ๋ํด ๊น์ด ํ๊ตฌํฉ๋๋ค. ๋ฒกํฐ ๊ฒ์ ๊ธฐ๋ฅ, ๋ฉํฐ ํ ๋ํธ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง, ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ๋ฅผ ์ํ Row Level Security(RLS)๋ฅผ ํฌํจํ ํฌ๊ด์ ์ธ ์๋งค ์คํค๋ง๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ MCP ์๋ฒ์ ๊ธฐ๋ฐ์ผ๋ก, ์ฌ๋ฌ ๋งค์ฅ์ ์๋งค ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด์ ์๊ฒฉํ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ๋ฅผ ์ ์งํฉ๋๋ค. ์ฐ๋ฆฌ๋ 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 ์๋ฒ ๊ตฌํ์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
PostgreSQL & pgvector
๋ฉํฐ ํ ๋ํธ ์ํคํ ์ฒ
๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค
---
์ด์ : Lab 03: ํ๊ฒฝ ์ค์
๋ค์: Lab 05: MCP ์๋ฒ ๊ตฌํ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
MCP ์๋ฒ ๊ตฌํ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ FastMCP ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ก๋์ ์์ค์ MCP ์๋ฒ๋ฅผ ๊ตฌํํ๋ ๊ณผ์ ์ ์๋ดํฉ๋๋ค. ํต์ฌ ์๋ฒ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ๊ตฌํํ๋ฉฐ, ๋ฐ์ดํฐ ์ก์ธ์ค๋ฅผ ์ํ ๋๊ตฌ๋ฅผ ๋ง๋ค๊ณ , AI ๊ธฐ๋ฐ ์๋งค ๋ถ์์ ์ํ ๊ธฐ์ด๋ฅผ ์ค์ ํ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ์๋ฒ๋ ์๋งค ๋ถ์ ์๋ฃจ์ ์ ์ค์ฌ์ ๋๋ค. ์ด ์๋ฒ๋ AI ์ด์์คํดํธ์ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ๋ค๋ฆฌ ์ญํ ์ ํ๋ฉฐ, ํ์คํ๋ ํ๋กํ ์ฝ์ ํตํด ๋น์ฆ๋์ค ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ ์ง๋ฅ์ ์ผ๋ก ์ก์ธ์คํ ์ ์๋๋ก ํฉ๋๋ค.
์ด ์ค์ต์์๋ ์ํฐํ๋ผ์ด์ฆ ํจํด๊ณผ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด๋ ๊ฒฌ๊ณ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
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()
์ฃผ์ ๊ตฌ์ฑ ๊ธฐ๋ฅ
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ๊ณ์ธต
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()
์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ณ์ธต ๊ธฐ๋ฅ
๐ง ์ฃผ์ 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 ์๋ฒ ๊ธฐ๋ฅ
๐ฅ ์ํ ๋ชจ๋ํฐ๋ง
์ํ ํ์ธ ๊ตฌํ (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: ๋๊ตฌ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
FastMCP ํ๋ ์์ํฌ
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
FastAPI ํจํด
---
๋ค์: ๋๊ตฌ๋ฅผ ํ์ฅํ ์ค๋น๊ฐ ๋์ จ๋์? Lab 06: ๋๊ตฌ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
MCP ์๋ฒ ๊ตฌํ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ FastMCP ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ก๋์ ์์ค์ MCP ์๋ฒ๋ฅผ ๊ตฌํํ๋ ๊ณผ์ ์ ์๋ดํฉ๋๋ค. ํต์ฌ ์๋ฒ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ๊ตฌํํ๋ฉฐ, ๋ฐ์ดํฐ ์ก์ธ์ค๋ฅผ ์ํ ๋๊ตฌ๋ฅผ ๋ง๋ค๊ณ , AI ๊ธฐ๋ฐ ์๋งค ๋ถ์์ ์ํ ๊ธฐ์ด๋ฅผ ์ค์ ํ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ์๋ฒ๋ ์๋งค ๋ถ์ ์๋ฃจ์ ์ ์ค์ฌ์ ๋๋ค. ์ด ์๋ฒ๋ AI ์ด์์คํดํธ์ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ๋ค๋ฆฌ ์ญํ ์ ํ๋ฉฐ, ํ์คํ๋ ํ๋กํ ์ฝ์ ํตํด ๋น์ฆ๋์ค ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ ์ง๋ฅ์ ์ผ๋ก ์ก์ธ์คํ ์ ์๋๋ก ํฉ๋๋ค.
์ด ์ค์ต์์๋ ์ํฐํ๋ผ์ด์ฆ ํจํด๊ณผ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด๋ ๊ฒฌ๊ณ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
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()
์ฃผ์ ๊ตฌ์ฑ ๊ธฐ๋ฅ
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ๊ณ์ธต
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()
์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ณ์ธต ๊ธฐ๋ฅ
๐ง ์ฃผ์ 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 ์๋ฒ ๊ธฐ๋ฅ
๐ฅ ์ํ ๋ชจ๋ํฐ๋ง
์ํ ํ์ธ ๊ตฌํ (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: ๋๊ตฌ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
FastMCP ํ๋ ์์ํฌ
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
FastAPI ํจํด
---
๋ค์: ๋๊ตฌ๋ฅผ ํ์ฅํ ์ค๋น๊ฐ ๋์ จ๋์? Lab 06: ๋๊ตฌ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋๊ตฌ ๊ฐ๋ฐ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์์๋ AI ์ด์์คํดํธ์๊ฒ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๊ธฐ๋ฅ, ์คํค๋ง ํ์, ๋ถ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๊ณ ๊ธ MCP ๋๊ตฌ๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๊น์ด ํ๊ตฌํฉ๋๋ค. ๊ฐ๋ ฅํ๋ฉด์๋ ์์ ํ ๋๊ตฌ๋ฅผ ์ค๊ณํ๊ณ , ํฌ๊ด์ ์ธ ์ค๋ฅ ์ฒ๋ฆฌ์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ๋๊ตฌ๋ AI ์ด์์คํดํธ์ ๋ฐ์ดํฐ ์์คํ ๊ฐ์ ์ธํฐํ์ด์ค ์ญํ ์ ํฉ๋๋ค. ์ ์ค๊ณ๋ ๋๊ตฌ๋ ๋ณต์กํ ์์ ์ ๋ํด ๊ตฌ์กฐํ๋๊ณ ๊ฒ์ฆ๋ ์ ๊ทผ์ ์ ๊ณตํ๋ฉฐ, ๋ณด์๊ณผ ์ฑ๋ฅ์ ์ ์งํฉ๋๋ค. ์ด ์ค์ต์์๋ ์ค๊ณ๋ถํฐ ๋ฐฐํฌ๊น์ง ๋๊ตฌ ๊ฐ๋ฐ์ ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด์ ๋ค๋ฃน๋๋ค.
์ฐ๋ฆฌ์ ์๋งค MCP ์๋ฒ๋ ํ๋งค ๋ฐ์ดํฐ, ์ ํ ์นดํ๋ก๊ทธ, ๋น์ฆ๋์ค ๋ถ์์ ์์ฐ์ด๋ก ์ฟผ๋ฆฌํ ์ ์๋ ํฌ๊ด์ ์ธ ๋๊ตฌ ์ธํธ๋ฅผ ๊ตฌํํ๋ฉฐ, ์๊ฒฉํ ๋ณด์ ๊ฒฝ๊ณ์ ์ต์ ์ ์ฑ๋ฅ์ ์ ์งํฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ๏ธ ํต์ฌ ๋๊ตฌ ์ํคํ ์ฒ
๋๊ตฌ ์ค๊ณ ์์น
# 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 ๋๊ตฌ ๊ฐ๋ฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณด์
์ฑ๋ฅ ์ต์ ํ
---
์ด์ : ์ค์ต 05: MCP ์๋ฒ ๊ตฌํ
๋ค์: ์ค์ต 07: ์๋งจํฑ ๊ฒ์ ํตํฉ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋๊ตฌ ๊ฐ๋ฐ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์์๋ AI ์ด์์คํดํธ์๊ฒ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๊ธฐ๋ฅ, ์คํค๋ง ํ์, ๋ถ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๊ณ ๊ธ MCP ๋๊ตฌ๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๊น์ด ํ๊ตฌํฉ๋๋ค. ๊ฐ๋ ฅํ๋ฉด์๋ ์์ ํ ๋๊ตฌ๋ฅผ ์ค๊ณํ๊ณ , ํฌ๊ด์ ์ธ ์ค๋ฅ ์ฒ๋ฆฌ์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ๋๊ตฌ๋ AI ์ด์์คํดํธ์ ๋ฐ์ดํฐ ์์คํ ๊ฐ์ ์ธํฐํ์ด์ค ์ญํ ์ ํฉ๋๋ค. ์ ์ค๊ณ๋ ๋๊ตฌ๋ ๋ณต์กํ ์์ ์ ๋ํด ๊ตฌ์กฐํ๋๊ณ ๊ฒ์ฆ๋ ์ ๊ทผ์ ์ ๊ณตํ๋ฉฐ, ๋ณด์๊ณผ ์ฑ๋ฅ์ ์ ์งํฉ๋๋ค. ์ด ์ค์ต์์๋ ์ค๊ณ๋ถํฐ ๋ฐฐํฌ๊น์ง ๋๊ตฌ ๊ฐ๋ฐ์ ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด์ ๋ค๋ฃน๋๋ค.
์ฐ๋ฆฌ์ ์๋งค MCP ์๋ฒ๋ ํ๋งค ๋ฐ์ดํฐ, ์ ํ ์นดํ๋ก๊ทธ, ๋น์ฆ๋์ค ๋ถ์์ ์์ฐ์ด๋ก ์ฟผ๋ฆฌํ ์ ์๋ ํฌ๊ด์ ์ธ ๋๊ตฌ ์ธํธ๋ฅผ ๊ตฌํํ๋ฉฐ, ์๊ฒฉํ ๋ณด์ ๊ฒฝ๊ณ์ ์ต์ ์ ์ฑ๋ฅ์ ์ ์งํฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ ๏ธ ํต์ฌ ๋๊ตฌ ์ํคํ ์ฒ
๋๊ตฌ ์ค๊ณ ์์น
# 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 ๋๊ตฌ ๊ฐ๋ฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณด์
์ฑ๋ฅ ์ต์ ํ
---
์ด์ : ์ค์ต 05: MCP ์๋ฒ ๊ตฌํ
๋ค์: ์ค์ต 07: ์๋งจํฑ ๊ฒ์ ํตํฉ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์๋งจํฑ ๊ฒ์ ํตํฉ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ Azure OpenAI ์๋ฒ ๋ฉ๊ณผ PostgreSQL์ pgvector ํ์ฅ์ ์ฌ์ฉํ์ฌ ์๋งจํฑ ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ดํดํ๊ณ ์๋งจํฑ ์ ์ฌ์ฑ์ ๊ธฐ๋ฐํ์ฌ ๊ด๋ จ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ๋ AI ๊ธฐ๋ฐ ์ ํ ๊ฒ์์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
์ ํต์ ์ธ ํค์๋ ๊ธฐ๋ฐ ๊ฒ์์ ์ฌ์ฉ์ ์๋์ ์๋งจํฑ ์๋ฏธ๋ฅผ ์ ๋๋ก ํ์ ํ์ง ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๋ฒกํฐ ์๋ฒ ๋ฉ์ ํ์ฉํ ์๋งจํฑ ๊ฒ์์ "๋น ์ค๋ ๋ ์ ์ ํฉํ ํธ์ํ ๋ฌ๋ํ"์ ๊ฐ์ ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ํตํด ์ ํ ์ค๋ช ์ ์ ํํ ๋์ผํ ๋จ์ด๊ฐ ํฌํจ๋์ง ์์๋ ๊ด๋ จ ์ ํ์ ์ฐพ์ ์ ์๋๋ก ํฉ๋๋ค.
์ฐ๋ฆฌ์ ๊ตฌํ์ Azure OpenAI์ ๊ฐ๋ ฅํ ์๋ฒ ๋ฉ ๋ชจ๋ธ๊ณผ PostgreSQL์ 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
๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค
์๋งจํฑ ๊ฒ์
---
์ด์ : ์ค์ต 06: ๋๊ตฌ ๊ฐ๋ฐ
๋ค์: ์ค์ต 08: ํ ์คํธ ๋ฐ ๋๋ฒ๊น
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์๋งจํฑ ๊ฒ์ ํตํฉ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ Azure OpenAI ์๋ฒ ๋ฉ๊ณผ PostgreSQL์ pgvector ํ์ฅ์ ์ฌ์ฉํ์ฌ ์๋งจํฑ ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ดํดํ๊ณ ์๋งจํฑ ์ ์ฌ์ฑ์ ๊ธฐ๋ฐํ์ฌ ๊ด๋ จ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ๋ AI ๊ธฐ๋ฐ ์ ํ ๊ฒ์์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
์ ํต์ ์ธ ํค์๋ ๊ธฐ๋ฐ ๊ฒ์์ ์ฌ์ฉ์ ์๋์ ์๋งจํฑ ์๋ฏธ๋ฅผ ์ ๋๋ก ํ์ ํ์ง ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๋ฒกํฐ ์๋ฒ ๋ฉ์ ํ์ฉํ ์๋งจํฑ ๊ฒ์์ "๋น ์ค๋ ๋ ์ ์ ํฉํ ํธ์ํ ๋ฌ๋ํ"์ ๊ฐ์ ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ํตํด ์ ํ ์ค๋ช ์ ์ ํํ ๋์ผํ ๋จ์ด๊ฐ ํฌํจ๋์ง ์์๋ ๊ด๋ จ ์ ํ์ ์ฐพ์ ์ ์๋๋ก ํฉ๋๋ค.
์ฐ๋ฆฌ์ ๊ตฌํ์ Azure OpenAI์ ๊ฐ๋ ฅํ ์๋ฒ ๋ฉ ๋ชจ๋ธ๊ณผ PostgreSQL์ 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
๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค
์๋งจํฑ ๊ฒ์
---
์ด์ : ์ค์ต 06: ๋๊ตฌ ๊ฐ๋ฐ
๋ค์: ์ค์ต 08: ํ ์คํธ ๋ฐ ๋๋ฒ๊น
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํ ์คํธ ๋ฐ ๋๋ฒ๊น
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ 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์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
ํ ์คํธ ํ๋ ์์ํฌ
์ฑ๋ฅ ํ ์คํธ
๋๋ฒ๊น ๋๊ตฌ
---
์ด์ : Lab 07: Semantic Search Integration
๋ค์: Lab 09: VS Code Integration
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
ํ ์คํธ ๋ฐ ๋๋ฒ๊น
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ 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์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
ํ ์คํธ ํ๋ ์์ํฌ
์ฑ๋ฅ ํ ์คํธ
๋๋ฒ๊น ๋๊ตฌ
---
์ด์ : Lab 07: Semantic Search Integration
๋ค์: Lab 09: VS Code Integration
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ์ ๋ขฐํ ์ ์๋ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
VS Code ํตํฉ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ๋ฅผ VS Code์ ํตํฉํ์ฌ AI ์ฑํ ์ ํตํ ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ํ์ฑํํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์ข ํฉ์ ์ธ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค. VS Code๋ฅผ MCP ์ฌ์ฉ์ ์ต์ ํํ๋๋ก ์ค์ ํ๊ณ , ์๋ฒ ์ฐ๊ฒฐ์ ๋๋ฒ๊น ํ๋ฉฐ, AI ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธ์์ฉ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
VS Code์ MCP ํตํฉ์ ๊ฐ๋ฐ์๊ฐ ์์ฐ์ด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ API๋ฅผ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ํ์ ์ ์ผ๋ก ๋ณํ์ํต๋๋ค. ์๋งค MCP ์๋ฒ๋ฅผ VS Code Chat์ ์ฐ๊ฒฐํ๋ฉด, ๋ํํ AI๋ฅผ ์ฌ์ฉํ์ฌ ํ๋งค ๋ฐ์ดํฐ, ์ ํ ์นดํ๋ก๊ทธ, ๋น์ฆ๋์ค ๋ถ์์ ์ง๋ฅ์ ์ผ๋ก ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค.
์ด ํตํฉ์ ํตํด ๊ฐ๋ฐ์๋ "์ด๋ฒ ๋ฌ์ ๊ฐ์ฅ ๋ง์ด ํ๋ฆฐ ์ ํ์ ๋ณด์ฌ์ค" ๋๋ "90์ผ ๋์ ๊ตฌ๋งคํ์ง ์์ ๊ณ ๊ฐ์ ์ฐพ์์ค"์ ๊ฐ์ ์ง๋ฌธ์ ํ๊ณ , SQL ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ง ์๊ณ ๋ ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ ์๋ต์ ๋ฐ์ ์ ์์ต๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ง 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: ๋ฐฐํฌ ์ ๋ต์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
VS Code ๊ฐ๋ฐ
MCP ํ๋กํ ์ฝ
๊ฐ๋ฐ ๋๊ตฌ
---
์ด์ : ์ค์ต 08: ํ ์คํธ ๋ฐ ๋๋ฒ๊น
๋ค์: ์ค์ต 10: ๋ฐฐํฌ ์ ๋ต
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
VS Code ํตํฉ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ MCP ์๋ฒ๋ฅผ VS Code์ ํตํฉํ์ฌ AI ์ฑํ ์ ํตํ ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ํ์ฑํํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์ข ํฉ์ ์ธ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค. VS Code๋ฅผ MCP ์ฌ์ฉ์ ์ต์ ํํ๋๋ก ์ค์ ํ๊ณ , ์๋ฒ ์ฐ๊ฒฐ์ ๋๋ฒ๊น ํ๋ฉฐ, AI ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธ์์ฉ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
VS Code์ MCP ํตํฉ์ ๊ฐ๋ฐ์๊ฐ ์์ฐ์ด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ API๋ฅผ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ํ์ ์ ์ผ๋ก ๋ณํ์ํต๋๋ค. ์๋งค MCP ์๋ฒ๋ฅผ VS Code Chat์ ์ฐ๊ฒฐํ๋ฉด, ๋ํํ AI๋ฅผ ์ฌ์ฉํ์ฌ ํ๋งค ๋ฐ์ดํฐ, ์ ํ ์นดํ๋ก๊ทธ, ๋น์ฆ๋์ค ๋ถ์์ ์ง๋ฅ์ ์ผ๋ก ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค.
์ด ํตํฉ์ ํตํด ๊ฐ๋ฐ์๋ "์ด๋ฒ ๋ฌ์ ๊ฐ์ฅ ๋ง์ด ํ๋ฆฐ ์ ํ์ ๋ณด์ฌ์ค" ๋๋ "90์ผ ๋์ ๊ตฌ๋งคํ์ง ์์ ๊ณ ๊ฐ์ ์ฐพ์์ค"์ ๊ฐ์ ์ง๋ฌธ์ ํ๊ณ , SQL ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ง ์๊ณ ๋ ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ ์๋ต์ ๋ฐ์ ์ ์์ต๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ง 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: ๋ฐฐํฌ ์ ๋ต์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
VS Code ๊ฐ๋ฐ
MCP ํ๋กํ ์ฝ
๊ฐ๋ฐ ๋๊ตฌ
---
์ด์ : ์ค์ต 08: ํ ์คํธ ๋ฐ ๋๋ฒ๊น
๋ค์: ์ค์ต 10: ๋ฐฐํฌ ์ ๋ต
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ฐฐํฌ ์ ๋ต
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ ํ๋์ ์ธ ์ปจํ ์ด๋ํ ๋ฐ ํด๋ผ์ฐ๋ ๋ค์ดํฐ๋ธ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํ์ฌ MCP ๋ฆฌํ ์ผ ์๋ฒ๋ฅผ ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํ๋ ํฌ๊ด์ ์ธ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ํฐํ๋ผ์ด์ฆ ์ํฌ๋ก๋๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ ํ์ฅ ๊ฐ๋ฅํ๊ณ ์์ ํ๋ฉฐ ๋ชจ๋ํฐ๋ง ๊ฐ๋ฅํ MCP ์๋ฒ๋ฅผ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ์๋ฒ์ ํ๋ก๋์ ๋ฐฐํฌ๋ ์ปจํ ์ด๋ํ, ์ค์ผ์คํธ๋ ์ด์ , ๋ณด์, ํ์ฅ์ฑ ๋ฐ ๋ชจ๋ํฐ๋ง์ ์ ์คํ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. ์ด ์ค์ต์์๋ Azure Container Apps์ PostgreSQL Flexible Server๋ฅผ ์ฌ์ฉํ ๋ฐฐํฌ, 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: ๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ธก์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
์ปจํ ์ด๋ ๊ธฐ์
CI/CD ๋ฐ DevOps
๋ณด์ ๋ฐ ๋ชจ๋ํฐ๋ง
---
์ด์ : Lab 09: VS Code ํตํฉ
๋ค์: Lab 11: ๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ธก
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ฐฐํฌ ์ ๋ต
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ ํ๋์ ์ธ ์ปจํ ์ด๋ํ ๋ฐ ํด๋ผ์ฐ๋ ๋ค์ดํฐ๋ธ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํ์ฌ MCP ๋ฆฌํ ์ผ ์๋ฒ๋ฅผ ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํ๋ ํฌ๊ด์ ์ธ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ํฐํ๋ผ์ด์ฆ ์ํฌ๋ก๋๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ ํ์ฅ ๊ฐ๋ฅํ๊ณ ์์ ํ๋ฉฐ ๋ชจ๋ํฐ๋ง ๊ฐ๋ฅํ MCP ์๋ฒ๋ฅผ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
MCP ์๋ฒ์ ํ๋ก๋์ ๋ฐฐํฌ๋ ์ปจํ ์ด๋ํ, ์ค์ผ์คํธ๋ ์ด์ , ๋ณด์, ํ์ฅ์ฑ ๋ฐ ๋ชจ๋ํฐ๋ง์ ์ ์คํ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. ์ด ์ค์ต์์๋ Azure Container Apps์ PostgreSQL Flexible Server๋ฅผ ์ฌ์ฉํ ๋ฐฐํฌ, 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: ๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ธก์ ๊ณ์ ์งํํ์ฌ:
๐ ์ถ๊ฐ ์๋ฃ
์ปจํ ์ด๋ ๊ธฐ์
CI/CD ๋ฐ DevOps
๋ณด์ ๋ฐ ๋ชจ๋ํฐ๋ง
---
์ด์ : Lab 09: VS Code ํตํฉ
๋ค์: Lab 11: ๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ธก
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผํด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ ํ๋ก๋์ ํ๊ฒฝ์์ MCP ์๋ฒ๋ฅผ ์ํ ๋ชจ๋ํฐ๋ง, ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ, ์๋ฆผ ๊ตฌํ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. Application Insights ์ค์ , ์ ์๋ฏธํ ๋์๋ณด๋ ์์ฑ, ํจ๊ณผ์ ์ธ ์๋ฆผ ๊ตฌํ, ์ด์ ์ฐ์์ฑ์ ์ํ ๋ฌธ์ ํด๊ฒฐ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
ํจ๊ณผ์ ์ธ ๋ชจ๋ํฐ๋ง๊ณผ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ์ ํ๋ก๋์ ํ๊ฒฝ์์ MCP ์๋ฒ์ ์์ ์ฑ์ ์ ์งํ๋ ๋ฐ ํ์์ ์ ๋๋ค. ์ด ์ค์ต์์๋ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ์ ์ธ ๊ฐ์ง ํต์ฌ ์์โ๋ฉํธ๋ฆญ, ๋ก๊ทธ, ํธ๋ ์ด์คโ๋ฅผ ๋ค๋ฃจ๋ฉฐ, ๋ฌธ์ ๋ฅผ ์ฌ์ ์ ๊ฐ์งํ๊ณ ์ ์ํ๊ฒ ํด๊ฒฐํ ์ ์๋ ํฌ๊ด์ ์ธ ๋ชจ๋ํฐ๋ง์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค.
์์ ํ ๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์์คํ ๋์์ ์ดํดํ๊ณ ์ฑ๋ฅ์ ์ต์ ํํ๋ฉฐ ๋์ ๊ฐ์ฉ์ฑ์ ๋ณด์ฅํ๋ ๋ฐ ๋์์ด ๋๋ ์คํ ๊ฐ๋ฅํ ์ธ์ฌ์ดํธ๋ก ๋ณํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ 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
OpenTelemetry
์ด์ ์ฐ์์ฑ
---
์ด์ : ์ค์ต 10: ๋ฐฐํฌ ์ ๋ต
๋ค์: ์ค์ต 12: ๋ชจ๋ฒ ์ฌ๋ก ๋ฐ ์ต์ ํ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ชจ๋ํฐ๋ง ๋ฐ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ค์ต์ ํ๋ก๋์ ํ๊ฒฝ์์ MCP ์๋ฒ๋ฅผ ์ํ ๋ชจ๋ํฐ๋ง, ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ, ์๋ฆผ ๊ตฌํ์ ๋ํ ํฌ๊ด์ ์ธ ์ง์นจ์ ์ ๊ณตํฉ๋๋ค. Application Insights ์ค์ , ์ ์๋ฏธํ ๋์๋ณด๋ ์์ฑ, ํจ๊ณผ์ ์ธ ์๋ฆผ ๊ตฌํ, ์ด์ ์ฐ์์ฑ์ ์ํ ๋ฌธ์ ํด๊ฒฐ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
๊ฐ์
ํจ๊ณผ์ ์ธ ๋ชจ๋ํฐ๋ง๊ณผ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ์ ํ๋ก๋์ ํ๊ฒฝ์์ MCP ์๋ฒ์ ์์ ์ฑ์ ์ ์งํ๋ ๋ฐ ํ์์ ์ ๋๋ค. ์ด ์ค์ต์์๋ ๊ด์ฐฐ ๊ฐ๋ฅ์ฑ์ ์ธ ๊ฐ์ง ํต์ฌ ์์โ๋ฉํธ๋ฆญ, ๋ก๊ทธ, ํธ๋ ์ด์คโ๋ฅผ ๋ค๋ฃจ๋ฉฐ, ๋ฌธ์ ๋ฅผ ์ฌ์ ์ ๊ฐ์งํ๊ณ ์ ์ํ๊ฒ ํด๊ฒฐํ ์ ์๋ ํฌ๊ด์ ์ธ ๋ชจ๋ํฐ๋ง์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค.
์์ ํ ๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์์คํ ๋์์ ์ดํดํ๊ณ ์ฑ๋ฅ์ ์ต์ ํํ๋ฉฐ ๋์ ๊ฐ์ฉ์ฑ์ ๋ณด์ฅํ๋ ๋ฐ ๋์์ด ๋๋ ์คํ ๊ฐ๋ฅํ ์ธ์ฌ์ดํธ๋ก ๋ณํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.
ํ์ต ๋ชฉํ
์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
๐ 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
OpenTelemetry
์ด์ ์ฐ์์ฑ
---
์ด์ : ์ค์ต 10: ๋ฐฐํฌ ์ ๋ต
๋ค์: ์ค์ต 12: ๋ชจ๋ฒ ์ฌ๋ก ๋ฐ ์ต์ ํ
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ชจ๋ฒ ์ฌ๋ก ๋ฐ ์ต์ ํ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ข ํฉ ์ค์ต์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ํตํด ๊ฐ๋ ฅํ๊ณ ํ์ฅ ๊ฐ๋ฅํ๋ฉฐ ์์ ํ 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 ์๋ฒ ๊ตฌ์ถ ํฌํจ:
๊ณ ๊ธ ํ์ต ๊ฒฝ๋ก
MCP ์ฌ์ ์ ๊ณ์ํ์ธ์:
์ปค๋ฎค๋ํฐ ์ธ์
์ฑ๊ณผ๋ฅผ ๊ณต์ ํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
๊ณ ๊ธ ์ฃผ์
๋ณด์ ์๋ฃ
์ปค๋ฎค๋ํฐ
---
๐ ์ถํํฉ๋๋ค! MCP ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ํ์ต ๊ฒฝ๋ก๋ฅผ ์์ฑํ์ต๋๋ค. ์ด์ AI ์ด์์คํดํธ์ ์ค์ ๋ฐ์ดํฐ ์์คํ ์ ์ฐ๊ฒฐํ๋ ํ๋ก๋์ ์ค๋น MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ์ง์๊ณผ ๊ธฐ์ ์ ๊ฐ์ถ๊ฒ ๋์์ต๋๋ค.
๊ธฐ์ฌํ ์ค๋น๊ฐ ๋์ จ๋์? ์ปค๋ฎค๋ํฐ์ ์ฐธ์ฌํ์ฌ ๊ฒฝํ์ ๊ณต์ ํ๊ฑฐ๋ ์ฝ๋ ๊ฐ์ ์ ๊ธฐ์ฌํ๊ฑฐ๋ ์ถ๊ฐ ํ์ต ์๋ฃ๋ฅผ ๋ง๋ค์ด ๋ค๋ฅธ ์ฌ๋๋ค์ด MCP๋ฅผ ๋ฐฐ์ฐ๋๋ก ๋์์ฃผ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๋ชจ๋ฒ ์ฌ๋ก ๋ฐ ์ต์ ํ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
์ด ์ข ํฉ ์ค์ต์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ํตํด ๊ฐ๋ ฅํ๊ณ ํ์ฅ ๊ฐ๋ฅํ๋ฉฐ ์์ ํ 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 ์๋ฒ ๊ตฌ์ถ ํฌํจ:
๊ณ ๊ธ ํ์ต ๊ฒฝ๋ก
MCP ์ฌ์ ์ ๊ณ์ํ์ธ์:
์ปค๋ฎค๋ํฐ ์ธ์
์ฑ๊ณผ๋ฅผ ๊ณต์ ํ์ธ์:
๐ ์ถ๊ฐ ์๋ฃ
๊ณ ๊ธ ์ฃผ์
๋ณด์ ์๋ฃ
์ปค๋ฎค๋ํฐ
---
๐ ์ถํํฉ๋๋ค! MCP ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ํ์ต ๊ฒฝ๋ก๋ฅผ ์์ฑํ์ต๋๋ค. ์ด์ AI ์ด์์คํดํธ์ ์ค์ ๋ฐ์ดํฐ ์์คํ ์ ์ฐ๊ฒฐํ๋ ํ๋ก๋์ ์ค๋น MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ์ง์๊ณผ ๊ธฐ์ ์ ๊ฐ์ถ๊ฒ ๋์์ต๋๋ค.
๊ธฐ์ฌํ ์ค๋น๊ฐ ๋์ จ๋์? ์ปค๋ฎค๋ํฐ์ ์ฐธ์ฌํ์ฌ ๊ฒฝํ์ ๊ณต์ ํ๊ฑฐ๋ ์ฝ๋ ๊ฐ์ ์ ๊ธฐ์ฌํ๊ฑฐ๋ ์ถ๊ฐ ํ์ต ์๋ฃ๋ฅผ ๋ง๋ค์ด ๋ค๋ฅธ ์ฌ๋๋ค์ด MCP๋ฅผ ๋ฐฐ์ฐ๋๋ก ๋์์ฃผ์ธ์.
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
๐ป ๊ตฌ์ถํ ๋ด์ฉ
ํ์ต ๊ฒฝ๋ก๋ฅผ ์๋ฃํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์์ ํ Zava ์๋งค ๋ถ์ 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. ํต์ฌ ๋ด์ฉ ๋ณต์ต - ๋ฐฐ์ด ๋ด์ฉ ํ์คํ ์ ๋ฆฌ
๋ชฉํ๋ณ ํ์ต
ํน์ ๊ธฐ์ ์ด ํ์ํ๋ฉด:
์ค์ต ์ค์ฌ ํ์ต
๊ฐ ์ค์ต์๋:
๐ ์ปค๋ฎค๋ํฐ์ ์ง์
๋์๋ฐ๊ธฐ
๐ ์์ํ ์ค๋น๊ฐ ๋์๋์?
์ง๊ธ ๋ฐ๋ก ์ค์ต 00: 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 ์๋ฒ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๋ฅด์นฉ๋๋ค. ์ด ์ค์ต์ ์๋ฃํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค: ํ๋์ AI ์ด์์คํดํธ๋ ๋งค์ฐ ๊ฐ๋ ฅํ์ง๋ง ์ค์ ๋น์ฆ๋์ค ๋ฐ์ดํฐ์ ์์
ํ ๋ ์ค์ํ ํ๊ณ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค: Model Context Protocol์ ๋ค์์ ํตํด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค: ์ด ํ์ต ๊ฒฝ๋ก์์๋ Zava Retail์ด๋ผ๋ ๊ฐ์์ DIY ์๋งค ์ฒด์ธ์ ์ํ MCP ์๋ฒ๋ฅผ ๊ตฌ์ถํฉ๋๋ค. ์ด ํ์ค์ ์ธ ์๋๋ฆฌ์ค๋ ์ํฐํ๋ผ์ด์ฆ๊ธ MCP ๊ตฌํ์ ๋ณด์ฌ์ค๋๋ค. Zava Retail์ ๋ค์์ ์ด์ํฉ๋๋ค: ๋งค์ฅ ๊ด๋ฆฌ์์ ์์์ AI ๊ธฐ๋ฐ ๋ถ์์ ํตํด ๋ค์์ ์ํํด์ผ ํฉ๋๋ค: 1. ๋งค์ฅ ๋ฐ ๊ธฐ๊ฐ๋ณ ํ๋งค ์ฑ๊ณผ ๋ถ์ 2. ์ฌ๊ณ ์์ค ์ถ์ ๋ฐ ์ฌ์
๊ณ ํ์์ฑ ์๋ณ 3. ๊ณ ๊ฐ ํ๋ ๋ฐ ๊ตฌ๋งค ํจํด ์ดํด 4. ์๋ฏธ ๊ฒ์์ ํตํ ์ ํ ํต์ฐฐ๋ ฅ ๋ฐ๊ฒฌ 5. ์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋ณด๊ณ ์ ์์ฑ 6. ์ญํ ๊ธฐ๋ฐ ์ก์ธ์ค ์ ์ด๋ฅผ ํตํ ๋ฐ์ดํฐ ๋ณด์ ์ ์ง MCP ์๋ฒ๋ ๋ค์์ ์ ๊ณตํด์ผ ํฉ๋๋ค: ์ฐ๋ฆฌ์ MCP ์๋ฒ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ์ต์ ํ๋ ๊ณ์ธตํ ์ํคํ
์ฒ๋ฅผ ๊ตฌํํฉ๋๋ค: ๋ค์ํ ์ฌ์ฉ์๊ฐ MCP ์๋ฒ์ ์ํธ์์ฉํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค: ์ฌ์ฉ์: Sarah, ์์ ํ ๋งค์ฅ ๊ด๋ฆฌ์ ๋ชฉํ: ์ง๋ ๋ถ๊ธฐ์ ํ๋งค ์ฑ๊ณผ ๋ถ์ ์์ฐ์ด ์ฟผ๋ฆฌ: > "2024๋
4๋ถ๊ธฐ ๋์ ๋ด ๋งค์ฅ์์ ๋งค์ถ ๊ธฐ์ค ์์ 10๊ฐ ์ ํ์ ๋ณด์ฌ์ค" ์งํ ๊ณผ์ : 1. VS Code AI ์ฑํ
์ด ์ฟผ๋ฆฌ๋ฅผ MCP ์๋ฒ๋ก ์ ์ก 2. MCP ์๋ฒ๊ฐ Sarah์ ๋งค์ฅ ์ปจํ
์คํธ(์์ ํ)๋ฅผ ์๋ณ 3. RLS ์ ์ฑ
์ด ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๋งค์ฅ์ผ๋ก ํํฐ๋ง 4. SQL ์ฟผ๋ฆฌ๊ฐ ์์ฑ๋๊ณ ์คํ๋จ 5. ๊ฒฐ๊ณผ๊ฐ ํฌ๋งท๋์ด AI ์ฑํ
์ผ๋ก ๋ฐํ 6. AI๊ฐ ๋ถ์ ๋ฐ ํต์ฐฐ๋ ฅ์ ์ ๊ณต ์ฌ์ฉ์: Mike, ์ฌ๊ณ ๊ด๋ฆฌ์ ๋ชฉํ: ๊ณ ๊ฐ ์์ฒญ๊ณผ ์ ์ฌํ ์ ํ ์ฐพ๊ธฐ ์์ฐ์ด ์ฟผ๋ฆฌ: > "์ผ์ธ์ฉ ๋ฐฉ์ ์ ๊ธฐ ์ปค๋ฅํฐ์ ์ ์ฌํ ์ ํ์ ์ฐ๋ฆฌ๊ฐ ํ๋งคํ๋์?" ์งํ ๊ณผ์ : 1. ์ฟผ๋ฆฌ๊ฐ ์๋ฏธ ๊ฒ์ ๋๊ตฌ์ ์ํด ์ฒ๋ฆฌ๋จ 2. Azure OpenAI๊ฐ ์๋ฒ ๋ฉ ๋ฒกํฐ๋ฅผ ์์ฑ 3. pgvector๊ฐ ์ ์ฌ์ฑ ๊ฒ์ ์ํ 4. ๊ด๋ จ ์ ํ์ด ๊ด๋ จ์ฑ ์์ผ๋ก ์ ๋ ฌ๋จ 5. ๊ฒฐ๊ณผ์ ์ ํ ์ธ๋ถ ์ ๋ณด์ ๊ฐ์ฉ์ฑ์ด ํฌํจ๋จ 6. AI๊ฐ ๋์ ๋ฐ ๋ฒ๋ค๋ง ๊ธฐํ๋ฅผ ์ ์ ์ฌ์ฉ์: Jennifer, ์ง์ญ ๊ด๋ฆฌ์ ๋ชฉํ: ๋ชจ๋ ๋งค์ฅ์ ์นดํ
๊ณ ๋ฆฌ๋ณ ํ๋งค ๋น๊ต ์์ฐ์ด ์ฟผ๋ฆฌ: > "์ง๋ 6๊ฐ์ ๋์ ๋ชจ๋ ๋งค์ฅ์ ์นดํ
๊ณ ๋ฆฌ๋ณ ํ๋งค๋ฅผ ๋น๊ตํด์ค" ์งํ ๊ณผ์ : 1. RLS ์ปจํ
์คํธ๊ฐ ์ง์ญ ๊ด๋ฆฌ์ ์ก์ธ์ค๋ก ์ค์ ๋จ 2. ๋ณต์กํ ๋ค์ค ๋งค์ฅ ์ฟผ๋ฆฌ๊ฐ ์์ฑ๋จ 3. ๋ฐ์ดํฐ๊ฐ ๋งค์ฅ ์์น๋ณ๋ก ์ง๊ณ๋จ 4. ๊ฒฐ๊ณผ์ ํธ๋ ๋์ ๋น๊ต๊ฐ ํฌํจ๋จ 5. AI๊ฐ ํต์ฐฐ๋ ฅ๊ณผ ์ถ์ฒ์ ์๋ณ ์ฐ๋ฆฌ์ ๊ตฌํ์ ์ํฐํ๋ผ์ด์ฆ๊ธ ๋ณด์์ ์ฐ์ ์ํฉ๋๋ค: PostgreSQL RLS๋ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์ฅํฉ๋๋ค: ๊ฐ MCP ์ฐ๊ฒฐ์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค: ๋ค์ค ๋ณด์ ๊ณ์ธต: ์ด ์๊ฐ๋ฅผ ์๋ฃํ ํ ๋ค์์ ์ดํดํด์ผ ํฉ๋๋ค: โ
MCP ๊ฐ์น ์ ์: MCP๊ฐ AI ์ด์์คํดํธ์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ โ
๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ: Zava Retail์ ์๊ตฌ ์ฌํญ๊ณผ ๊ณผ์ โ
์ํคํ
์ฒ ๊ฐ์: ์ฃผ์ ๊ตฌ์ฑ ์์์ ์ํธ์์ฉ โ
๊ธฐ์ ์คํ: ์ฌ์ฉ๋ ๋๊ตฌ์ ํ๋ ์์ํฌ โ
๋ณด์ ๋ชจ๋ธ: ๋ฉํฐ ํ
๋ํธ ๋ฐ์ดํฐ ์ก์ธ์ค ๋ฐ ๋ณดํธ โ
์ฌ์ฉ ํจํด: ์ค์ ์ฟผ๋ฆฌ ์๋๋ฆฌ์ค์ ์ํฌํ๋ก ๋ ๊น์ด ํ๊ตฌํ ์ค๋น๊ฐ ๋์
จ๋์? ๋ค์์ ์งํํ์ธ์: Lab 01: ํต์ฌ ์ํคํ
์ฒ ๊ฐ๋
MCP ์๋ฒ ์ํคํ
์ฒ ํจํด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น, ์๋งค ๋ถ์ ์๋ฃจ์
์ ์ง์ํ๋ ์์ธ ๊ธฐ์ ๊ตฌํ์ ๋ํด ์์๋ณด์ธ์. --- ๋ฉด์ฑ
์กฐํญ: ์ด๋ ๊ฐ์์ ์๋งค ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ํ์ต ์ฐ์ต์
๋๋ค. ํ๋ก๋์
ํ๊ฒฝ์์ ์ ์ฌํ ์๋ฃจ์
์ ๊ตฌํํ ๋๋ ํญ์ ์กฐ์ง์ ๋ฐ์ดํฐ ๊ฑฐ๋ฒ๋์ค ๋ฐ ๋ณด์ ์ ์ฑ
์ ๋ฐ๋ฅด์ญ์์ค. --- ๋ฉด์ฑ
์กฐํญ: ์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค. ์ ํ์ฑ์ ์ํด ์ต์ ์ ๋คํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ด ํฌํจ๋ ์ ์์ต๋๋ค. ์๋ณธ ๋ฌธ์์ ์์ด ๋ฒ์ ์ด ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค. ์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ, ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค. ์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด ๋น์ฌ๋ ์ฑ
์์ ์ง์ง ์์ต๋๋ค.MCP ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ์๊ฐ
๐ฏ ์ด ์ค์ต์์ ๋ค๋ฃจ๋ ๋ด์ฉ
๊ฐ์
ํ์ต ๋ชฉํ
๐งญ ๋์ ๊ณผ์ : AI์ ์ค์ ๋ฐ์ดํฐ์ ๋ง๋จ
๊ธฐ์กด AI์ ํ๊ณ
๋์ ๊ณผ์
์ค๋ช
๋น์ฆ๋์ค ์ํฅ
---------------
-----------------
-------------------
์ ์ ์ง์
๊ณ ์ ๋ ๋ฐ์ดํฐ์
์ผ๋ก ํ๋ จ๋ AI ๋ชจ๋ธ์ ํ์ฌ ๋น์ฆ๋์ค ๋ฐ์ดํฐ๋ฅผ ์ก์ธ์คํ ์ ์์
์ค๋๋ ํต์ฐฐ๋ ฅ, ๊ธฐํ ์์ค
๋ฐ์ดํฐ ์ฌ์ผ๋ก
๋ฐ์ดํฐ๋ฒ ์ด์ค, API, ์์คํ
์ ์ ๊ธด ์ ๋ณด๋ก ์ธํด AI๊ฐ ์ ๊ทผ ๋ถ๊ฐ
๋ถ์์ ํ ๋ถ์, ๋จํธํ๋ ์ํฌํ๋ก
๋ณด์ ์ ์ฝ
์ง์ ์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ก์ธ์ค๋ ๋ณด์ ๋ฐ ๊ท์ ์ค์ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐ
์ ํ๋ ๋ฐฐํฌ, ์๋ ๋ฐ์ดํฐ ์ค๋น
๋ณต์กํ ์ฟผ๋ฆฌ
๋น์ฆ๋์ค ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ ํต์ฐฐ๋ ฅ์ ์ถ์ถํ๋ ค๋ฉด ๊ธฐ์ ์ ์ง์์ด ํ์
๋ฎ์ ์ฑํ๋ฅ , ๋นํจ์จ์ ์ธ ํ๋ก์ธ์ค
MCP ์๋ฃจ์
๐ช Zava Retail ์๊ฐ: ํ์ต ์ฌ๋ก ์ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail
๋น์ฆ๋์ค ๋ฐฐ๊ฒฝ
๋น์ฆ๋์ค ์๊ตฌ ์ฌํญ
๊ธฐ์ ์๊ตฌ ์ฌํญ
๐๏ธ 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 ์๋ฒ ๊ณ์ธต
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ๊ณ์ธต
3. ๋ณด์ ๊ณ์ธต
4. AI ๊ฐํ ๊ณ์ธต
๐ง ๊ธฐ์ ์คํ
ํต์ฌ ๊ธฐ์
๊ตฌ์ฑ ์์
๊ธฐ์
๋ชฉ์
---------------
----------------
-------------
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
๋น๋ฐ ๋ฐ ๊ตฌ์ฑ ๊ด๋ฆฌ
๐ฌ ์ค์ ์ฌ์ฉ ์๋๋ฆฌ์ค
์๋๋ฆฌ์ค 1: ๋งค์ฅ ๊ด๋ฆฌ์ ์ฑ๊ณผ ๊ฒํ
์๋๋ฆฌ์ค 2: ์๋ฏธ ๊ฒ์์ ํตํ ์ ํ ๋ฐ๊ฒฌ
์๋๋ฆฌ์ค 3: ๋งค์ฅ ๊ฐ ๋ถ์
๐ ๋ณด์ ๋ฐ ๋ฉํฐ ํ
๋์ ์ฌ์ธต ๋ถ์
Row Level Security (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 ๋ฌธ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
Azure ์๋น์ค
---
*๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ํตํ ํ๋ก๋์ ๊ธ MCP ์๋ฒ ๊ตฌ์ถ์ ์ด ํฌ๊ด์ ์ด๊ณ ์ค์ต ์ค์ฌ์ ํ์ต ๊ฒฝํ์์ ๋ง์คํฐํ์ธ์.*
---
๋ฉด์ฑ ์กฐํญ:
์ด ๋ฌธ์๋ AI ๋ฒ์ญ ์๋น์ค Co-op Translator๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ผ๋, ์๋ ๋ฒ์ญ์ ์ค๋ฅ๋ ๋ถ์ ํ์ฑ์ ํฌํจํ ์ ์์์ ์๋ ค๋๋ฆฝ๋๋ค.
์๋ณธ ๋ฌธ์์ ์์ด๊ฐ ๊ถ์ ์๋ ์ถ์ฒ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
์ด ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ๋ชจ๋ ์คํด๋ ์ค์ญ์ ๋ํด ๋น์ฌ๋ ์ฑ ์์ ์ง์ง ์์ต๋๋ค.
์คํฐ๋ ๊ฐ์ด๋
์ด๋ณด์๋ฅผ ์ํ ๋ชจ๋ธ ์ปจํ ์คํธ ํ๋กํ ์ฝ(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 ๊ธฐ๋ฐ ๋ถ์, ๊ธฐ์ ๋ณด์
์ถ๊ฐ ๋ฆฌ์์ค
๋ฆฌํฌ์งํ ๋ฆฌ์๋ ๋ค์์ ์ง์ ๋ฆฌ์์ค๊ฐ ํฌํจ๋์ด ์์ต๋๋ค:
์ด ๋ฆฌํฌ์งํ ๋ฆฌ ์ฌ์ฉ ๋ฐฉ๋ฒ
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๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒ์ญ๋์์ต๋๋ค.
์ ํ์ฑ์ ์ํด ๋ ธ๋ ฅํ๊ณ ์์ง๋ง, ์๋ ๋ฒ์ญ์๋ ์ค๋ฅ๋ ๋ถ์ ํํ ๋ถ๋ถ์ด ์์ ์ ์์์ ์ ์ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์๋ณธ ๋ฌธ์๋ ํด๋น ์ธ์ด๋ก ๋ ์๋ฌธ์ด ๊ถ์ ์๋ ์๋ฃ๋ก ๊ฐ์ฃผ๋์ด์ผ ํฉ๋๋ค.
์ค์ํ ์ ๋ณด์ ๊ฒฝ์ฐ ์ ๋ฌธ์ ์ธ ์ธ๊ฐ ๋ฒ์ญ์ ๊ถ์ฅํฉ๋๋ค.
๋ณธ ๋ฒ์ญ ์ฌ์ฉ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ์คํด๋ ์๋ชป๋ ํด์์ ๋ํด์๋ ๋น์ฌ๊ฐ ์ฑ ์์ง์ง ์์ต๋๋ค.