81 KiB
Najlepsze praktyki rozwoju MCP
(Kliknij powyższy obrazek, aby obejrzeć wideo z tej lekcji)
Przegląd
Ta lekcja koncentruje się na zaawansowanych najlepszych praktykach dotyczących tworzenia, testowania i wdrażania serwerów i funkcji MCP w środowiskach produkcyjnych. W miarę jak ekosystemy MCP stają się coraz bardziej złożone i istotne, przestrzeganie ustalonych wzorców zapewnia niezawodność, łatwość konserwacji i interoperacyjność. Ta lekcja konsoliduje praktyczną wiedzę zdobytą podczas rzeczywistych implementacji MCP, aby pomóc Ci tworzyć solidne, wydajne serwery z efektywnymi zasobami, podpowiedziami i narzędziami.
Cele nauki
Po zakończeniu tej lekcji będziesz potrafił:
- Zastosować najlepsze praktyki branżowe w projektowaniu serwerów i funkcji MCP
- Tworzyć kompleksowe strategie testowania serwerów MCP
- Projektować wydajne, wielokrotnego użytku wzorce przepływów pracy dla złożonych aplikacji MCP
- Wdrażać odpowiednie obsługi błędów, logowanie i obserwowalność w serwerach MCP
- Optymalizować implementacje MCP pod kątem wydajności, bezpieczeństwa i konserwowalności
Podstawowe zasady MCP
Zanim przejdziesz do konkretnych zasad implementacji, ważne jest, aby zrozumieć podstawowe zasady kierujące skutecznym rozwojem MCP:
-
Standaryzowana komunikacja: MCP korzysta z JSON-RPC 2.0 jako podstawy, zapewniając spójny format żądań, odpowiedzi oraz obsługi błędów we wszystkich implementacjach.
-
Projektowanie zorientowane na użytkownika: Zawsze priorytetowo traktuj zgodę użytkownika, kontrolę i przejrzystość w swoich implementacjach MCP.
-
Bezpieczeństwo przede wszystkim: Wdrażaj solidne środki bezpieczeństwa, w tym uwierzytelnianie, autoryzację, walidację i ograniczanie liczby żądań.
-
Architektura modułowa: Projektuj serwery MCP w modularny sposób, gdzie każde narzędzie i zasób ma jasny, skoncentrowany cel.
-
Połączenia stanowe: Wykorzystuj zdolność MCP do utrzymywania stanu pomiędzy wieloma żądaniami dla bardziej spójnych i świadomych kontekstu interakcji.
Oficjalne najlepsze praktyki MCP
Poniższe najlepsze praktyki pochodzą z oficjalnej dokumentacji Model Context Protocol:
Najlepsze praktyki bezpieczeństwa
-
Zgoda i kontrola użytkownika: Zawsze wymagaj wyraźnej zgody użytkownika przed dostępem do danych lub wykonywaniem operacji. Zapewnij jasną kontrolę nad tym, jakie dane są udostępniane i które działania są autoryzowane.
-
Prywatność danych: Udostępniaj dane użytkownika tylko za wyraźną zgodą i chroń je odpowiednimi kontrolami dostępu. Zabezpieczaj przed nieautoryzowanym przesyłaniem danych.
-
Bezpieczeństwo narzędzi: Wymagaj wyraźnej zgody użytkownika przed wywołaniem jakiegokolwiek narzędzia. Upewnij się, że użytkownicy rozumieją funkcje każdego narzędzia i egzekwuj solidne granice bezpieczeństwa.
-
Kontrola uprawnień narzędzi: Konfiguruj, które narzędzia model może używać podczas sesji, zapewniając dostęp tylko do wyraźnie autoryzowanych narzędzi.
-
Uwierzytelnianie: Wymagaj odpowiedniego uwierzytelniania przed udzieleniem dostępu do narzędzi, zasobów lub wrażliwych operacji za pomocą kluczy API, tokenów OAuth lub innych bezpiecznych metod uwierzytelniania.
-
Walidacja parametrów: Wymuszaj walidację dla wszystkich wywołań narzędzi, aby zapobiec przekazaniu sfałszowanych lub złośliwych danych do implementacji narzędzi.
-
Ograniczanie liczby żądań: Wdrażaj ograniczenia liczby żądań, aby zapobiec nadużyciom i zapewnić uczciwe wykorzystanie zasobów serwera.
Najlepsze praktyki implementacji
-
Negocjacja możliwości: Podczas nawiązywania połączenia wymieniaj informacje o obsługiwanych funkcjach, wersjach protokołu, dostępnych narzędziach i zasobach.
-
Projektowanie narzędzi: Twórz skoncentrowane narzędzia, które wykonują jedną rzecz dobrze, zamiast monolitycznych narzędzi obsługujących wiele aspektów.
-
Obsługa błędów: Wdrażaj ustandaryzowane komunikaty i kody błędów, które pomagają diagnozować problemy, obsługiwać błędy w sposób łagodny i dostarczać konstruktywne informacje zwrotne.
-
Logowanie: Konfiguruj strukturalne logi do audytu, debugowania i monitorowania interakcji protokołu.
-
Śledzenie postępu: Dla długotrwałych operacji raportuj aktualizacje postępu, aby umożliwić responsywny interfejs użytkownika.
-
Anulowanie żądań: Pozwól klientom anulować trwające żądania, które nie są już potrzebne lub zajmują zbyt dużo czasu.
Dodatkowe źródła
Aby uzyskać najbardziej aktualne informacje o najlepszych praktykach MCP, zapoznaj się z:
- Dokumentacja MCP
- Specyfikacja MCP (2025-11-25)
- Repozytorium GitHub
- Najlepsze praktyki bezpieczeństwa
- OWASP MCP Top 10 - Ryzyka bezpieczeństwa i środki zaradcze
- Warsztat MCP Security Summit (Sherpa) - Praktyczne szkolenie z bezpieczeństwa
Praktyczne przykłady implementacji
Najlepsze praktyki projektowania narzędzi
1. Zasada pojedynczej odpowiedzialności
Każde narzędzie MCP powinno mieć jasny, skoncentrowany cel. Zamiast tworzyć monolityczne narzędzia, które próbują obsłużyć wiele kwestii, rozwijaj specjalistyczne narzędzia, które doskonale radzą sobie z konkretnymi zadaniami.
// 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. Spójna obsługa błędów
Wdrażaj solidną obsługę błędów z informacyjnymi komunikatami błędów i odpowiednimi mechanizmami odzyskiwania.
# Przykład w Pythonie z kompleksową obsługą błędów
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:
# Walidacja parametrów
if "query" not in parameters:
raise ToolParameterError("Missing required parameter: query")
query = parameters["query"]
# Walidacja bezpieczeństwa
if self._contains_unsafe_sql(query):
raise ToolSecurityError("Query contains potentially unsafe SQL")
try:
# Operacja na bazie danych z limitem czasu
async with timeout(10): # Limit czasu 10 sekund
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:
# Błędy połączenia mogą być przejściowe
self._log_error("Database connection error", e)
raise ToolExecutionError(f"Database connection error: {str(e)}")
except DatabaseQueryError as e:
# Błędy zapytania prawdopodobnie są błędami klienta
self._log_error("Database query error", e)
raise ToolExecutionError(f"Invalid query: {str(e)}")
except ToolError:
# Pozwól przejść błędom specyficznym dla narzędzia
raise
except Exception as e:
# Obsługa wszystkich nieoczekiwanych błędów
self._log_error("Unexpected error in DataQueryTool", e)
raise ToolExecutionError(f"An unexpected error occurred: {str(e)}")
def _contains_unsafe_sql(self, query):
# Implementacja wykrywania wstrzyknięć SQL
pass
def _log_error(self, message, error):
# Implementacja logowania błędów
pass
3. Walidacja parametrów
Zawsze dokładnie waliduj parametry, aby zapobiec przekazaniu niepoprawnych lub złośliwych danych.
// Przykład JavaScript/TypeScript z szczegółową walidacją parametrów
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. Walidacja obecności parametru
if (!parameters.operation) {
throw new ToolError("Missing required parameter: operation");
}
if (!parameters.path) {
throw new ToolError("Missing required parameter: path");
}
// 2. Walidacja typów parametrów
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. Walidacja wartości parametrów
const validOperations = ["read", "write", "delete"];
if (!validOperations.includes(parameters.operation)) {
throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
}
// 4. Walidacja obecności zawartości dla operacji zapisu
if (parameters.operation === "write" && !parameters.content) {
throw new ToolError("Content parameter is required for write operation");
}
// 5. Walidacja bezpieczeństwa ścieżki
if (!this.isPathWithinAllowedDirectories(parameters.path)) {
throw new ToolError("Access denied: path is outside of allowed directories");
}
// Implementacja oparta na zweryfikowanych parametrach
// ...
}
isPathWithinAllowedDirectories(path) {
// Implementacja sprawdzania bezpieczeństwa ścieżki
// ...
}
}
Przykłady implementacji bezpieczeństwa
1. Uwierzytelnianie i autoryzacja
// Przykład w Javie z uwierzytelnianiem i autoryzacją
public class SecureDataAccessTool implements Tool {
private final AuthenticationService authService;
private final AuthorizationService authzService;
private final DataService dataService;
// Wstrzykiwanie zależności
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. Wyodrębnij kontekst uwierzytelniania
String authToken = request.getContext().getAuthToken();
// 2. Uwierzytelnij użytkownika
UserIdentity user;
try {
user = authService.validateToken(authToken);
} catch (AuthenticationException e) {
return ToolResponse.error("Authentication failed: " + e.getMessage());
}
// 3. Sprawdź autoryzację dla konkretnej operacji
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. Kontynuuj z autoryzowaną operacją
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. Ograniczanie liczby żądań
// 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
// ...
}
}
Najlepsze praktyki testowania
1. Testowanie jednostkowe narzędzi MCP
Zawsze testuj narzędzia w izolacji, wykorzystując makiety (mocki) zależności zewnętrznych:
// Przykład testu jednostkowego narzędzia w TypeScript
describe('WeatherForecastTool', () => {
let tool: WeatherForecastTool;
let mockWeatherService: jest.Mocked<IWeatherService>;
beforeEach(() => {
// Utwórz mock serwisu pogodowego
mockWeatherService = {
getForecasts: jest.fn()
} as any;
// Utwórz narzędzie z mock zależnością
tool = new WeatherForecastTool(mockWeatherService);
});
it('should return weather forecast for a location', async () => {
// Przygotuj
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);
// Wykonaj
const response = await tool.execute({
location: 'Seattle',
days: 3
});
// Sprawdź
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 () => {
// Przygotuj
mockWeatherService.getForecasts.mockRejectedValue(new Error('Service unavailable'));
// Wykonaj i sprawdź
await expect(tool.execute({
location: 'Seattle',
days: 3
})).rejects.toThrow('Weather service error: Service unavailable');
});
});
2. Testowanie integracyjne
Testuj pełny przebieg od żądań klienta do odpowiedzi serwera:
# Przykład testu integracyjnego w Pythonie
@pytest.mark.asyncio
async def test_mcp_server_integration():
# Uruchom serwer testowy
server = McpServer()
server.register_tool(WeatherForecastTool(MockWeatherService()))
await server.start(port=5000)
try:
# Utwórz klienta
client = McpClient("http://localhost:5000")
# Przetestuj wykrywanie narzędzia
tools = await client.discover_tools()
assert "weatherForecast" in [t.name for t in tools]
# Przetestuj wykonanie narzędzia
response = await client.execute_tool("weatherForecast", {
"location": "Seattle",
"days": 3
})
# Zweryfikuj odpowiedź
assert response.status_code == 200
assert "Seattle" in response.content[0].text
assert len(json.loads(response.content[0].text)["forecasts"]) == 3
finally:
# Posprzątaj
await server.stop()
Optymalizacja wydajności
1. Strategie buforowania
Wdrażaj odpowiednie buforowanie, aby zmniejszyć opóźnienia i wykorzystanie zasobów:
// 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. Wstrzykiwanie zależności i testowalność
Projektuj narzędzia tak, aby otrzymywały swoje zależności przez wstrzykiwanie w konstruktorze, co ułatwia testowanie i konfigurację:
// Przykład Java z wstrzykiwaniem zależności
public class CurrencyConversionTool implements Tool {
private final ExchangeRateService exchangeService;
private final CacheService cacheService;
private final Logger logger;
// Zależności wstrzykiwane przez konstruktor
public CurrencyConversionTool(
ExchangeRateService exchangeService,
CacheService cacheService,
Logger logger) {
this.exchangeService = exchangeService;
this.cacheService = cacheService;
this.logger = logger;
}
// Implementacja narzędzia
// ...
}
3. Komponowalne narzędzia
Projektuj narzędzia tak, aby można je było łączyć w celu tworzenia bardziej złożonych przepływów pracy:
# Przykład Pythona pokazujący narzędzia kompozycyjne
class DataFetchTool(Tool):
def get_name(self):
return "dataFetch"
# Implementacja...
class DataAnalysisTool(Tool):
def get_name(self):
return "dataAnalysis"
# To narzędzie może korzystać z wyników narzędzia dataFetch
async def execute_async(self, request):
# Implementacja...
pass
class DataVisualizationTool(Tool):
def get_name(self):
return "dataVisualize"
# To narzędzie może korzystać z wyników narzędzia dataAnalysis
async def execute_async(self, request):
# Implementacja...
pass
# Te narzędzia mogą być używane niezależnie lub jako część przepływu pracy
Najlepsze praktyki projektowania schematów
Schemat to kontrakt pomiędzy modelem a Twoim narzędziem. Dobrze zaprojektowane schematy prowadzą do lepszej użyteczności narzędzi.
1. Jasne opisy parametrów
Zawsze dołączaj opisowe informacje dla każdego parametru:
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. Ograniczenia walidacji
Uwzględnij ograniczenia walidacyjne, aby zapobiec nieprawidłowym danym wejściowym:
Map<String, Object> getSchema() {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>();
// Właściwość e-mail z walidacją formatu
Map<String, Object> email = new HashMap<>();
email.put("type", "string");
email.put("format", "email");
email.put("description", "User email address");
// Właściwość wieku z ograniczeniami numerycznymi
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");
// Właściwość enumerowana
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. Spójne struktury odpowiedzi
Utrzymuj spójność w strukturach odpowiedzi, aby ułatwić modelom interpretację wyników:
async def execute_async(self, request):
try:
# Przetwarzaj żądanie
results = await self._search_database(request.parameters["query"])
# Zawsze zwracaj spójną strukturę
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
}
Obsługa błędów
Solidna obsługa błędów jest kluczowa dla utrzymania niezawodności narzędzi MCP.
1. Łagodna obsługa błędów
Obsługuj błędy na odpowiednich poziomach i dostarczaj informacyjne komunikaty:
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. Strukturalne odpowiedzi błędów
Zwracaj strukturalne informacje o błędach, gdy jest to możliwe:
@Override
public ToolResponse execute(ToolRequest request) {
try {
// Implementacja
} 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();
}
// Ponowne wyrzucenie innych wyjątków jako ToolExecutionException
throw new ToolExecutionException("Tool execution failed: " + ex.getMessage(), ex);
}
}
3. Logika ponawiania prób
Wdrażaj odpowiednią logikę ponawiania prób dla błędów przejściowych:
async def execute_async(self, request):
max_retries = 3
retry_count = 0
base_delay = 1 # sekundy
while retry_count < max_retries:
try:
# Wywołaj zewnętrzne 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)}")
# Eksponencjalne opóźnienie zwrotne
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:
# Błąd nietrwały, nie ponawiaj prób
raise ToolExecutionException(f"Operation failed: {str(e)}")
Optymalizacja wydajności
1. Buforowanie
Wdrażaj buforowanie dla kosztownych operacji:
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. Przetwarzanie asynchroniczne
Stosuj wzorce programowania asynchronicznego dla operacji blokujących 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();
// Dla operacji długotrwałych natychmiast zwróć identyfikator przetwarzania
String processId = UUID.randomUUID().toString();
// Rozpocznij asynchroniczne przetwarzanie
CompletableFuture.runAsync(() -> {
try {
// Wykonaj operację długotrwałą
documentService.processDocument(documentId);
// Zaktualizuj status (zazwyczaj byłby przechowywany w bazie danych)
processStatusRepository.updateStatus(processId, "completed");
} catch (Exception ex) {
processStatusRepository.updateStatus(processId, "failed", ex.getMessage());
}
}, executorService);
// Zwroc natychmiastową odpowiedź z identyfikatorem procesu
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();
}
// Narzędzie towarzyszące do sprawdzania statusu
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. Ograniczanie zasobów
Wdrażaj ograniczenia zasobów, aby zapobiec przeciążeniu:
class ThrottledApiTool(Tool):
def __init__(self):
self.rate_limiter = TokenBucketRateLimiter(
tokens_per_second=5, # Zezwól na 5 żądań na sekundę
bucket_size=10 # Zezwól na nagłe skoki do 10 żądań
)
async def execute_async(self, request):
# Sprawdź, czy możemy kontynuować, czy trzeba czekać
delay = self.rate_limiter.get_delay_time()
if delay > 0:
if delay > 2.0: # Jeśli czas oczekiwania jest zbyt długi
raise ToolExecutionException(
f"Rate limit exceeded. Please try again in {delay:.1f} seconds."
)
else:
# Poczekaj odpowiedni czas opóźnienia
await asyncio.sleep(delay)
# Zużyj token i kontynuuj żądanie
self.rate_limiter.consume()
# Wywołaj 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
# Oblicz czas do kolejnego dostępnego tokena
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
# Dodaj nowe tokeny na podstawie upływającego czasu
new_tokens = elapsed * self.tokens_per_second
self.tokens = min(self.bucket_size, self.tokens + new_tokens)
self.last_refill = now
Najlepsze praktyki bezpieczeństwa
1. Walidacja wejścia
Zawsze dokładnie waliduj parametry wejściowe:
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. Kontrole autoryzacji
Wdrażaj właściwe kontrole autoryzacji:
@Override
public ToolResponse execute(ToolRequest request) {
// Pobierz kontekst użytkownika z żądania
UserContext user = request.getContext().getUserContext();
// Sprawdź, czy użytkownik ma wymagane uprawnienia
if (!authorizationService.hasPermission(user, "documents:read")) {
throw new ToolExecutionException("User does not have permission to access documents");
}
// Dla określonych zasobów sprawdź dostęp do tego zasobu
String documentId = request.getParameters().get("documentId").asText();
if (!documentService.canUserAccess(user.getId(), documentId)) {
throw new ToolExecutionException("Access denied to the requested document");
}
// Kontynuuj wykonanie narzędzia
// ...
}
3. Obsługa danych wrażliwych
Obchodź się ostrożnie z danymi wrażliwymi:
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)
# Pobierz dane użytkownika
user_data = await self.user_service.get_user_data(user_id)
# Filtruj poufne pola, chyba że zostały wyraźnie zażądane I zatwierdzone
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):
# Sprawdź poziom uprawnień w kontekście żądania
auth_level = request.context.get("authorizationLevel")
return auth_level == "admin"
def _redact_sensitive_fields(self, user_data):
# Utwórz kopię, aby uniknąć modyfikowania oryginału
redacted = user_data.copy()
# Zatuszuj konkretne poufne pola
sensitive_fields = ["ssn", "creditCardNumber", "password"]
for field in sensitive_fields:
if field in redacted:
redacted[field] = "REDACTED"
# Zatuszuj zagnieżdżone dane poufne
if "financialInfo" in redacted:
redacted["financialInfo"] = {"available": True, "accessRestricted": True}
return redacted
Najlepsze praktyki testowania narzędzi MCP
Kompleksowe testowanie zapewnia prawidłowe działanie narzędzi MCP, obsługę przypadków granicznych oraz prawidłową integrację z resztą systemu.
Testowanie jednostkowe
1. Testuj każde narzędzie w izolacji
Twórz skoncentrowane testy funkcjonalności każdego narzędzia:
[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. Testowanie walidacji schematów
Testuj poprawność schematów i ich egzekwowanie ograniczeń:
@Test
public void testSchemaValidation() {
// Utwórz instancję narzędzia
SearchTool searchTool = new SearchTool();
// Pobierz schemat
Object schema = searchTool.getSchema();
// Przekonwertuj schemat na JSON do walidacji
String schemaJson = objectMapper.writeValueAsString(schema);
// Sprawdź, czy schemat jest prawidłowym JSONSchema
JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
JsonSchema jsonSchema = factory.getJsonSchema(schemaJson);
// Przetestuj prawidłowe parametry
JsonNode validParams = objectMapper.createObjectNode()
.put("query", "test query")
.put("limit", 5);
ProcessingReport validReport = jsonSchema.validate(validParams);
assertTrue(validReport.isSuccess());
// Przetestuj brakujący wymagany parametr
JsonNode missingRequired = objectMapper.createObjectNode()
.put("limit", 5);
ProcessingReport missingReport = jsonSchema.validate(missingRequired);
assertFalse(missingReport.isSuccess());
// Przetestuj nieprawidłowy typ parametru
JsonNode invalidType = objectMapper.createObjectNode()
.put("query", "test")
.put("limit", "not-a-number");
ProcessingReport invalidReport = jsonSchema.validate(invalidType);
assertFalse(invalidReport.isSuccess());
}
3. Testy obsługi błędów
Twórz specyficzne testy dla sytuacji błędnych:
@pytest.mark.asyncio
async def test_api_tool_handles_timeout():
# Ustaw
tool = ApiTool(timeout=0.1) # Bardzo krótki limit czasu
# Zamockuj żądanie, które przekroczy limit czasu
with aioresponses() as mocked:
mocked.get(
"https://api.example.com/data",
callback=lambda *args, **kwargs: asyncio.sleep(0.5) # Dłużej niż limit czasu
)
request = ToolRequest(
tool_name="apiTool",
parameters={"url": "https://api.example.com/data"}
)
# Wykonaj i potwierdź
with pytest.raises(ToolExecutionException) as exc_info:
await tool.execute_async(request)
# Sprawdź wiadomość wyjątku
assert "timed out" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_api_tool_handles_rate_limiting():
# Ustaw
tool = ApiTool()
# Zamockuj odpowiedź z ograniczeniem szybkości
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"}
)
# Wykonaj i potwierdź
with pytest.raises(ToolExecutionException) as exc_info:
await tool.execute_async(request)
# Sprawdź, czy wyjątek zawiera informacje o ograniczeniu szybkości
error_msg = str(exc_info.value).lower()
assert "rate limit" in error_msg
assert "try again" in error_msg
Testowanie integracyjne
1. Testowanie łańcucha narzędzi
Testuj narzędzia pracujące razem w oczekiwanych kombinacjach:
[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. Testowanie serwera MCP
Testuj serwer MCP z pełną rejestracją i wykonywaniem narzędzi:
@SpringBootTest
@AutoConfigureMockMvc
public class McpServerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testToolDiscovery() throws Exception {
// Przetestuj punkt końcowy odkrywania
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 {
// Utwórz żądanie narzędzia
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);
// Wyślij żądanie i zweryfikuj odpowiedź
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 {
// Utwórz nieprawidłowe żądanie narzędzia
Map<String, Object> request = new HashMap<>();
request.put("toolName", "calculator");
Map<String, Object> parameters = new HashMap<>();
parameters.put("operation", "divide");
parameters.put("a", 10);
// Brakujący parametr "b"
request.put("parameters", parameters);
// Wyślij żądanie i zweryfikuj odpowiedź błędu
mockMvc.perform(post("/mcp/execute")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").exists());
}
}
3. Testy end-to-end
Testuj kompletne przepływy od podpowiedzi modelu do wykonania narzędzia:
@pytest.mark.asyncio
async def test_model_interaction_with_tool():
# Ustaw - Skonfiguruj klienta MCP i model mock
mcp_client = McpClient(server_url="http://localhost:5000")
# Odpowiedzi modelu mock
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=[]
)
])
# Odpowiedź narzędzia pogodowego mock
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"}
]
}
}
)
# Działaj
response = await mcp_client.send_prompt(
"What's the weather in Seattle?",
model=mock_model,
allowed_tools=["weatherForecast"]
)
# Sprawdź
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"
Testowanie wydajności
1. Testy obciążeniowe
Testuj, ile równoczesnych żądań może obsłużyć Twój serwer 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. Testy wytrzymałościowe
Testuj system pod ekstremalnym obciążeniem:
@Test
public void testServerUnderStress() {
int maxUsers = 1000;
int rampUpTimeSeconds = 60;
int testDurationSeconds = 300;
// Skonfiguruj JMeter do testów obciążeniowych
StandardJMeterEngine jmeter = new StandardJMeterEngine();
// Skonfiguruj plan testów JMeter
HashTree testPlanTree = new HashTree();
// Utwórz plan testów, grupę wątków, próbniki itp.
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);
// Dodaj próbnika HTTP do wykonania narzędzia
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);
// Dodaj nasłuchiwacze
SummaryReport summaryReport = new SummaryReport();
threadGroup.add(summaryReport);
// Uruchom test
jmeter.configure(testPlanTree);
jmeter.run();
// Zweryfikuj wyniki
assertEquals(0, summaryReport.getErrorCount());
assertTrue(summaryReport.getAverage() < 200); // Średni czas odpowiedzi < 200 ms
assertTrue(summaryReport.getPercentile(90.0) < 500); // 90. percentyl < 500 ms
}
3. Monitorowanie i profilowanie
Konfiguruj monitorowanie dla długoterminowej analizy wydajności:
# Skonfiguruj monitorowanie serwera MCP
def configure_monitoring(server):
# Skonfiguruj metryki 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"]
)
}
# Dodaj middleware do mierzenia czasu i rejestrowania metryk
server.add_middleware(PrometheusMiddleware(prometheus_metrics))
# Udostępnij punkt końcowy metryk
@server.router.get("/metrics")
async def metrics():
return generate_latest()
return server
Wzorce projektowe przepływów MCP
Dobrze zaprojektowane przepływy MCP poprawiają wydajność, niezawodność i konserwowalność. Oto kluczowe wzorce do stosowania:
1. Wzorzec łańcucha narzędzi
Łącz kilka narzędzi w sekwencję, gdzie wynik jednego narzędzia staje się wejściem dla kolejnego:
# Implementacja łańcucha narzędzi w Pythonie
class ChainWorkflow:
def __init__(self, tools_chain):
self.tools_chain = tools_chain # Lista nazw narzędzi do wykonania w kolejności
async def execute(self, mcp_client, initial_input):
current_result = initial_input
all_results = {"input": initial_input}
for tool_name in self.tools_chain:
# Wykonaj każde narzędzie w łańcuchu, przekazując poprzedni wynik
response = await mcp_client.execute_tool(tool_name, current_result)
# Przechowaj wynik i użyj go jako dane wejściowe dla następnego narzędzia
all_results[tool_name] = response.result
current_result = response.result
return {
"final_result": current_result,
"all_results": all_results
}
# Przykład użycia
data_processing_chain = ChainWorkflow([
"dataFetch",
"dataCleaner",
"dataAnalyzer",
"dataVisualizer"
])
result = await data_processing_chain.execute(
mcp_client,
{"source": "sales_database", "table": "transactions"}
)
2. Wzorzec dyspozytora
Używaj centralnego narzędzia, które kieruje zapytania do wyspecjalizowanych narzędzi na podstawie danych wejściowych:
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. Wzorzec przetwarzania równoległego
Uruchamiaj wiele narzędzi jednocześnie dla efektywności:
public class ParallelDataProcessingWorkflow {
private final McpClient mcpClient;
public ParallelDataProcessingWorkflow(McpClient mcpClient) {
this.mcpClient = mcpClient;
}
public WorkflowResult execute(String datasetId) {
// Krok 1: Pobierz metadane zestawu danych (synchronizacja)
ToolResponse metadataResponse = mcpClient.executeTool("datasetMetadata",
Map.of("datasetId", datasetId));
// Krok 2: Uruchom wiele analiz równolegle
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"
))
);
// Poczekaj na zakończenie wszystkich zadań równoległych
CompletableFuture<Void> allAnalyses = CompletableFuture.allOf(
statisticalAnalysis, correlationAnalysis, outlierDetection
);
allAnalyses.join(); // Czekaj na zakończenie
// Krok 3: Połącz wyniki
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());
// Krok 4: Wygeneruj raport podsumowujący
ToolResponse summaryResponse = mcpClient.executeTool("reportGenerator",
Map.of("analysisResults", combinedResults));
// Zwróć pełny wynik przepływu pracy
WorkflowResult result = new WorkflowResult();
result.setDatasetId(datasetId);
result.setAnalysisResults(combinedResults);
result.setSummaryReport(summaryResponse.getResult());
return result;
}
}
4. Wzorzec odzyskiwania z błędów
Wdrażaj łagodne mechanizmy zapasowe dla awarii narzędzi:
class ResilientWorkflow:
def __init__(self, mcp_client):
self.client = mcp_client
async def execute_with_fallback(self, primary_tool, fallback_tool, parameters):
try:
# Najpierw spróbuj narzędzia podstawowego
response = await self.client.execute_tool(primary_tool, parameters)
return {
"result": response.result,
"source": "primary",
"tool": primary_tool
}
except ToolExecutionException as e:
# Zaloguj błąd
logging.warning(f"Primary tool '{primary_tool}' failed: {str(e)}")
# Przełącz się na narzędzie zapasowe
try:
# Może być konieczna transformacja parametrów dla narzędzia zapasowego
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:
# Oba narzędzia nie powiodły się
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"""
# Ta implementacja zależałaby od konkretnych narzędzi
# W tym przykładzie po prostu zwrócimy oryginalne parametry
return params
# Przykład użycia
async def get_weather(workflow, location):
return await workflow.execute_with_fallback(
"premiumWeatherService", # Podstawowe (płatne) API pogodowe
"basicWeatherService", # Zapasowe (darmowe) API pogodowe
{"location": location}
)
5. Wzorzec komponowania przepływów
Buduj złożone przepływy pracy, komponując prostsze:
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" }
});
Testowanie serwerów MCP: najlepsze praktyki i kluczowe wskazówki
Przegląd
Testowanie jest krytycznym aspektem tworzenia niezawodnych, wysokiej jakości serwerów MCP. Niniejszy przewodnik dostarcza kompleksowych najlepszych praktyk i wskazówek dotyczących testowania serwerów MCP na wszystkich etapach rozwoju, od testów jednostkowych po testy integracyjne i walidację end-to-end.
Dlaczego testowanie jest ważne w serwerach MCP
Serwery MCP pełnią kluczową rolę jako middleware pomiędzy modelami AI a aplikacjami klienckimi. Dokładne testowanie zapewnia:
- Niezawodność w środowiskach produkcyjnych
- Poprawne przetwarzanie żądań i odpowiedzi
- Właściwą implementację specyfikacji MCP
- Odporność na awarie i przypadki brzegowe
- Stałą wydajność przy różnych obciążeniach
Testowanie jednostkowe serwerów MCP
Testowanie jednostkowe (fundament)
Testy jednostkowe weryfikują pojedyncze komponenty serwera MCP w izolacji.
Co testować
- Obsługiwacze zasobów: testuj logikę każdego obsługiwacza zasobów niezależnie
- Implementacje narzędzi: sprawdzaj zachowanie narzędzi z różnymi wejściami
- Szablony podpowiedzi: upewnij się, że szablony podpowiedzi renderują się poprawnie
- Walidacja schematów: testuj logikę walidacji parametrów
- Obsługa błędów: weryfikuj odpowiedzi na nieprawidłowe dane
Najlepsze praktyki testów jednostkowych
// 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);
}
# Przykładowy test jednostkowy dla narzędzia kalkulator w Pythonie
def test_calculator_tool_add():
# Przygotuj
calculator = CalculatorTool()
parameters = {
"operation": "add",
"a": 5,
"b": 7
}
# Wykonaj
response = calculator.execute(parameters)
result = json.loads(response.content[0].text)
# Sprawdź
assert result["value"] == 12
Testowanie integracyjne (warstwa środkowa)
Testy integracyjne sprawdzają interakcje między komponentami serwera MCP.
Co testować
- Inicjalizacja serwera: testuj uruchamianie serwera z różnymi konfiguracjami
- Rejestracja tras: sprawdzaj poprawną rejestrację wszystkich punktów końcowych
- Przetwarzanie żądań: testuj pełny cykl żądanie-odpowiedź
- Propagacja błędów: upewnij się, że błędy są właściwie obsługiwane między komponentami
- Uwierzytelnianie i autoryzacja: testuj mechanizmy bezpieczeństwa
Najlepsze praktyki testów integracyjnych
// 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();
}
Testowanie end-to-end (warstwa najwyższa)
Testy end-to-end weryfikują zachowanie całego systemu od klienta do serwera.
Co testować
- Komunikacja klient-serwer: testuj pełne cykle żądanie-odpowiedź
- Rzeczywiste SDK klientów: testuj z rzeczywistymi implementacjami klienta
- Wydajność pod obciążeniem: weryfikuj zachowanie przy wielu równoczesnych żądaniach
- Odzyskiwanie po błędach: testuj przywracanie systemu po awariach
- Operacje długotrwałe: weryfikuj obsługę strumieniowania i długich operacji
Najlepsze praktyki testów E2E
// Przykładowy test E2E z klientem w TypeScript
describe('MCP Server E2E Tests', () => {
let client: McpClient;
beforeAll(async () => {
// Uruchom serwer w środowisku testowym
await startTestServer();
client = new McpClient('http://localhost:5000');
});
afterAll(async () => {
await stopTestServer();
});
test('Client can invoke calculator tool and get correct result', async () => {
// Działanie
const response = await client.invokeToolAsync('calculator', {
operation: 'divide',
a: 20,
b: 4
});
// Sprawdzenie oczekiwań
expect(response.statusCode).toBe(200);
expect(response.content[0].text).toContain('5');
});
});
Strategie makietowania (mocking) w testach MCP
Makietowanie jest niezbędne do izolowania komponentów podczas testów.
Komponenty do makietowania
- Zewnętrzne modele AI: makietuj odpowiedzi modeli dla przewidywalnego testowania
- Zewnętrzne usługi: makietuj zależności API (bazy danych, usługi stron trzecich)
- Usługi uwierzytelniania: makietuj dostawców tożsamości
- Dostawcy zasobów: makietuj kosztowne obsługiwacze zasobów
Przykład: makietowanie odpowiedzi modelu 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);
# Przykład Pythona z unittest.mock
@patch('mcp_server.models.OpenAIModel')
def test_with_mock_model(mock_model):
# Skonfiguruj mock
mock_model.return_value.generate_response.return_value = {
"text": "Mocked model response",
"finish_reason": "completed"
}
# Użyj mocka w teście
server = McpServer(model_client=mock_model)
# Kontynuuj test
Testowanie wydajności
Testowanie wydajności jest kluczowe dla produkcyjnych serwerów MCP.
Co mierzyć
- Opóźnienie: czas odpowiedzi na żądania
- Przepustowość: liczba obsłużonych żądań na sekundę
- Wykorzystanie zasobów: użycie CPU, pamięci, sieci
- Obsługa współbieżności: zachowanie przy równoległych żądaniach
- Charakterystyka skalowania: wydajność przy rosnącym obciążeniu
Narzędzia do testów wydajności
- k6: narzędzie open-source do testów obciążeniowych
- JMeter: kompleksowe testy wydajności
- Locust: testy obciążeniowe oparte na Pythonie
- Azure Load Testing: testy wydajności w chmurze
Przykład: podstawowy test obciążeniowy z k6
// skrypt k6 do testowania obciążenia serwera MCP
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 użytkowników wirtualnych
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);
}
Automatyzacja testów serwerów MCP
Automatyzacja testów zapewnia stałą jakość i szybsze pętle informacji zwrotnych.
Integracja CI/CD
- Uruchamiaj testy jednostkowe przy pull requestach: upewnij się, że zmiany w kodzie nie psują istniejącej funkcjonalności
- Testy integracyjne w stagingu: Uruchamiaj testy integracyjne w środowiskach przedprodukcyjnych
- Bazowe wskaźniki wydajności: Utrzymuj benchmarki wydajnościowe, aby wychwytywać regresje
- Skanowanie bezpieczeństwa: Automatyzuj testy bezpieczeństwa jako część pipeline'u
Przykładowy pipeline 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
Testowanie zgodności ze specyfikacją MCP
Zweryfikuj, czy Twój serwer poprawnie implementuje specyfikację MCP.
Kluczowe obszary zgodności
- Punkty końcowe API: Testuj wymagane endpointy (/resources, /tools, itd.)
- Format żądania/odpowiedzi: Weryfikuj zgodność ze schematem
- Kody błędów: Sprawdzaj poprawność kodów statusu dla różnych scenariuszy
- Typy treści: Testuj obsługę różnych typów treści
- Proces uwierzytelniania: Weryfikuj mechanizmy uwierzytelniania zgodne ze specyfikacją
Zestaw testów zgodności
[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
});
}
10 najlepszych wskazówek do skutecznego testowania serwera MCP
- Testuj definicje narzędzi oddzielnie: Weryfikuj definicje schematów niezależnie od logiki narzędzi
- Używaj testów parametryzowanych: Testuj narzędzia z różnorodnymi wejściami, w tym przypadkami brzegowymi
- Sprawdzaj odpowiedzi błędów: Weryfikuj prawidłową obsługę błędów we wszystkich możliwych przypadkach
- Testuj logikę autoryzacji: Zapewnij właściwą kontrolę dostępu dla różnych ról użytkowników
- Monitoruj pokrycie testów: Dąż do wysokiego pokrycia kodu krytycznych ścieżek
- Testuj odpowiedzi streamingowe: Weryfikuj prawidłową obsługę strumieniowanej treści
- Symuluj problemy sieciowe: Testuj zachowanie w warunkach słabego połączenia
- Testuj limity zasobów: Sprawdzaj zachowanie przy osiąganiu limitów kwot lub limitów zapytań
- Automatyzuj testy regresji: Buduj zestaw, który uruchamia się przy każdej zmianie kodu
- Dokumentuj przypadki testowe: Utrzymuj jasną dokumentację scenariuszy testowych
Typowe pułapki podczas testowania
- Nadmierne poleganie na testach „happy path”: Upewnij się, że testujesz dokładnie także przypadki błędów
- Ignorowanie testów wydajności: Wykrywaj wąskie gardła zanim wpłyną na produkcję
- Testowanie wyłącznie w izolacji: Łącz testy jednostkowe, integracyjne i end-to-end
- Niepełne pokrycie API: Zapewnij testowanie wszystkich endpointów i funkcji
- Niespójne środowiska testowe: Używaj kontenerów, aby zapewnić spójność środowisk testowych
Podsumowanie
Kompleksowa strategia testowania jest niezbędna do tworzenia niezawodnych, wysokiej jakości serwerów MCP. Wdrażając najlepsze praktyki i wskazówki zawarte w tym przewodniku, możesz mieć pewność, że Twoje implementacje MCP spełnią najwyższe standardy jakości, niezawodności i wydajności.
Kluczowe wnioski
- Projektowanie narzędzi: Stosuj zasadę pojedynczej odpowiedzialności, używaj wstrzykiwania zależności i projektuj pod kątem kompozycyjności
- Projektowanie schematu: Twórz jasne, dobrze udokumentowane schematy z odpowiednimi ograniczeniami walidacji
- Obsługa błędów: Implementuj łagodne zarządzanie błędami, ustrukturyzowane odpowiedzi błędów oraz mechanizm ponawiania
- Wydajność: Korzystaj z cache'owania, asynchronicznego przetwarzania i ograniczania zasobów
- Bezpieczeństwo: Stosuj dokładną walidację danych wejściowych, kontrole autoryzacji oraz właściwą obsługę danych wrażliwych
- Testowanie: Twórz kompleksowe testy jednostkowe, integracyjne i end-to-end
- Wzorce workflow: Stosuj sprawdzone wzorce, takie jak łańcuchy, dispatcher i przetwarzanie równoległe
Ćwiczenie
Zaprojektuj narzędzie MCP oraz workflow dla systemu przetwarzania dokumentów, które:
- Przyjmuje dokumenty w wielu formatach (PDF, DOCX, TXT)
- Ekstrahuje tekst i kluczowe informacje z dokumentów
- Klasyfikuje dokumenty według typu i zawartości
- Generuje podsumowanie każdego dokumentu
Zaimplementuj schematy narzędzia, obsługę błędów oraz wzorzec workflow najlepiej odpowiadający temu scenariuszowi. Zastanów się, jak przetestowałbyś tę implementację.
Zasoby
- Dołącz do społeczności MCP na Microsoft Foundry Discord Community, aby być na bieżąco z najnowszymi informacjami
- Wspieraj projekty open-source MCP
- Stosuj zasady MCP we własnych inicjatywach AI w swojej organizacji
- Poznaj specjalistyczne implementacje MCP dla swojej branży
- Rozważ udział w zaawansowanych kursach na wybrane tematy MCP, takie jak integracja multimodalna czy integracja aplikacji korporacyjnych
- Eksperymentuj z budowaniem własnych narzędzi i workflow MCP, korzystając z zasad przedstawionych w Hands on Lab
Co dalej
Następny rozdział: Studia przypadków
Zastrzeżenie: Niniejszy dokument został przetłumaczony za pomocą usługi tłumaczenia AI Co-op Translator. Choć dążymy do dokładności, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub niedokładności. Oryginalny dokument w jego języku źródłowym należy uznawać za autorytatywne źródło. W przypadku informacji krytycznych zalecane jest skorzystanie z profesjonalnego tłumaczenia wykonanego przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.
