79 KiB
MCP Ontwikkelings Beste Praktijken
(Klik op de bovenstaande afbeelding om de video van deze les te bekijken)
Overzicht
Deze les richt zich op geavanceerde beste praktijken voor het ontwikkelen, testen en implementeren van MCP-servers en -functies in productieomgevingen. Naarmate MCP-ecosystemen in complexiteit en belang toenemen, zorgt het volgen van vastgestelde patronen voor betrouwbaarheid, onderhoudbaarheid en interoperabiliteit. Deze les verzamelt praktische kennis die is opgedaan uit echte MCP-implementaties om je te begeleiden bij het creëren van robuuste, efficiënte servers met effectieve resources, prompts en tools.
Leerdoelen
Aan het einde van deze les kun je:
- Industrie beste praktijken toepassen bij het ontwerpen van MCP-servers en -functies
- Uitgebreide teststrategieën creëren voor MCP-servers
- Efficiënte, herbruikbare workflowpatronen ontwerpen voor complexe MCP-toepassingen
- Correcte foutafhandeling, logging en observatie implementeren in MCP-servers
- MCP-implementaties optimaliseren voor prestaties, veiligheid en onderhoudbaarheid
MCP Kernprincipes
Voordat we in specifieke implementatiepraktijken duiken, is het belangrijk om de kernprincipes te begrijpen die effectieve MCP-ontwikkeling sturen:
-
Gestandaardiseerde Communicatie: MCP gebruikt JSON-RPC 2.0 als basis, dat een consistent formaat biedt voor verzoeken, antwoorden en foutafhandeling in alle implementaties.
-
Gebruikersgerichte Ontwerp: Geef altijd prioriteit aan gebruikers-toestemming, controle en transparantie in je MCP-implementaties.
-
Veiligheid Eerst: Implementeer robuuste beveiligingsmaatregelen waaronder authenticatie, autorisatie, validatie en snelheidsbeperking.
-
Modulaire Architectuur: Ontwerp je MCP-servers modulair, waarbij elke tool en resource een duidelijke, gerichte rol heeft.
-
Statusvolle Verbindingen: Maak gebruik van MCP's vermogen om status over meerdere verzoeken te behouden voor coherente en contextbewuste interacties.
Officiële MCP Beste Praktijken
De volgende beste praktijken zijn afgeleid uit de officiële Model Context Protocol-documentatie:
Veiligheid Beste Praktijken
-
Gebruikers Toestemming en Controle: Vereis altijd expliciete gebruikers-toestemming voordat data wordt geraadpleegd of bewerkingen worden uitgevoerd. Bied duidelijke controle over welke data wordt gedeeld en welke acties zijn geautoriseerd.
-
Dataprivacy: Stel gebruikersdata alleen bloot met expliciete toestemming en bescherm deze met passende toegangscontroles. Bescherm tegen ongeautoriseerde datatransmissie.
-
Tool Veiligheid: Vereis expliciete gebruikers-toestemming voordat een tool wordt aangeroepen. Zorg dat gebruikers de functionaliteit van elke tool begrijpen en handhaaf robuuste beveiligingsgrenzen.
-
Tool Toestemmingscontrole: Configureer welke tools een model mag gebruiken tijdens een sessie, zodat alleen expliciet geautoriseerde tools toegankelijk zijn.
-
Authenticatie: Vereis correcte authenticatie voordat toegang wordt verleend tot tools, resources of gevoelige operaties via API-sleutels, OAuth-tokens of andere beveiligde authenticatiemethoden.
-
Parametervalidatie: Voer validatie uit voor alle tool-aanroepen om te voorkomen dat foutieve of kwaadaardige invoer bij tool-implementaties terechtkomt.
-
Snelheidsbeperking (Rate Limiting): Implementeer snelheidsbeperking om misbruik te voorkomen en eerlijk gebruik van serverresources te waarborgen.
Implementatie Beste Praktijken
-
Capabiliteitsonderhandeling: Wissel tijdens het opzetten van verbinding informatie uit over ondersteunde functies, protocolversies, beschikbare tools en resources.
-
Toolontwerp: Maak gerichte tools die één ding goed doen, in plaats van monolithische tools die meerdere zorgen behandelen.
-
Foutafhandeling: Implementeer gestandaardiseerde foutmeldingen en codes om problemen te diagnosticeren, fouten gracieus af te handelen en bruikbare feedback te bieden.
-
Logging: Configureer gestructureerde logs voor auditing, debugging en monitoring van protocolinteracties.
-
Voortgangsbewaking: Rapporteer voortgangsupdates bij langlopende operaties om responsieve gebruikersinterfaces mogelijk te maken.
-
Annulering van Verzoeken: Sta clients toe om lopende verzoeken te annuleren die niet langer nodig zijn of te lang duren.
Aanvullende Referenties
Voor de meest actuele informatie over MCP beste praktijken, raadpleeg:
- MCP Documentation
- MCP Specification (2025-11-25)
- GitHub Repository
- Security Best Practices
- OWASP MCP Top 10 - Beveiligingsrisico's en mitigaties
- MCP Security Summit Workshop (Sherpa) - Praktische security training
Praktische Implementatie Voorbeelden
Toolontwerp Beste Praktijken
1. Single Responsibility Principle
Elke MCP-tool moet een duidelijke, gerichte rol hebben. In plaats van monolithische tools te maken die meerdere verantwoordelijkheden proberen te beheren, ontwikkel gespecialiseerde tools die uitblinken in specifieke taken.
// 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. Consistente Foutafhandeling
Implementeer robuuste foutafhandeling met informatieve foutmeldingen en passende herstelmechanismen.
# Python voorbeeld met uitgebreide foutafhandeling
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:
# Parametervalidatie
if "query" not in parameters:
raise ToolParameterError("Missing required parameter: query")
query = parameters["query"]
# Beveiligingsvalidatie
if self._contains_unsafe_sql(query):
raise ToolSecurityError("Query contains potentially unsafe SQL")
try:
# Databasebewerking met time-out
async with timeout(10): # 10 seconden time-out
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:
# Verbindingsfouten kunnen tijdelijk zijn
self._log_error("Database connection error", e)
raise ToolExecutionError(f"Database connection error: {str(e)}")
except DatabaseQueryError as e:
# Queryfouten zijn waarschijnlijk clientfouten
self._log_error("Database query error", e)
raise ToolExecutionError(f"Invalid query: {str(e)}")
except ToolError:
# Laat toolspecifieke fouten passeren
raise
except Exception as e:
# Vang alle onverwachte fouten op
self._log_error("Unexpected error in DataQueryTool", e)
raise ToolExecutionError(f"An unexpected error occurred: {str(e)}")
def _contains_unsafe_sql(self, query):
# Implementatie van SQL-injectiedetectie
pass
def _log_error(self, message, error):
# Implementatie van foutlogging
pass
3. Parametervalidatie
Valideer altijd parameters grondig om foutieve of kwaadaardige invoer te voorkomen.
// JavaScript/TypeScript voorbeeld met gedetailleerde parametervalidatie
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. Controleer aanwezigheid van parameter
if (!parameters.operation) {
throw new ToolError("Missing required parameter: operation");
}
if (!parameters.path) {
throw new ToolError("Missing required parameter: path");
}
// 2. Controleer 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. Controleer parameter waarden
const validOperations = ["read", "write", "delete"];
if (!validOperations.includes(parameters.operation)) {
throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
}
// 4. Controleer aanwezigheid van inhoud voor schrijfoperatie
if (parameters.operation === "write" && !parameters.content) {
throw new ToolError("Content parameter is required for write operation");
}
// 5. Validatie van padveiligheid
if (!this.isPathWithinAllowedDirectories(parameters.path)) {
throw new ToolError("Access denied: path is outside of allowed directories");
}
// Implementatie op basis van gevalideerde parameters
// ...
}
isPathWithinAllowedDirectories(path) {
// Implementatie van padveiligheidscontrole
// ...
}
}
Veiligheidsimplementatie Voorbeelden
1. Authenticatie en Autorisatie
// Java voorbeeld met authenticatie en autorisatie
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. Haal authenticatiecontext op
String authToken = request.getContext().getAuthToken();
// 2. Authenticeer gebruiker
UserIdentity user;
try {
user = authService.validateToken(authToken);
} catch (AuthenticationException e) {
return ToolResponse.error("Authentication failed: " + e.getMessage());
}
// 3. Controleer autorisatie voor de specifieke handeling
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. Ga door met geautoriseerde handeling
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. Snelheidsbeperking
// 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
// ...
}
}
Test Beste Praktijken
1. Unit Testing MCP Tools
Test je tools altijd geïsoleerd, met mocken van externe afhankelijkheden:
// TypeScript voorbeeld van een tool eenheidstest
describe('WeatherForecastTool', () => {
let tool: WeatherForecastTool;
let mockWeatherService: jest.Mocked<IWeatherService>;
beforeEach(() => {
// Maak een nep weerservice
mockWeatherService = {
getForecasts: jest.fn()
} as any;
// Maak de tool met de neponderdeel
tool = new WeatherForecastTool(mockWeatherService);
});
it('should return weather forecast for a location', async () => {
// Arrangeer
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);
// Voer uit
const response = await tool.execute({
location: 'Seattle',
days: 3
});
// Bevestig
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 () => {
// Arrangeer
mockWeatherService.getForecasts.mockRejectedValue(new Error('Service unavailable'));
// Voer uit & Bevestig
await expect(tool.execute({
location: 'Seattle',
days: 3
})).rejects.toThrow('Weather service error: Service unavailable');
});
});
2. Integratietesten
Test de complete flow van clientverzoeken tot serverantwoorden:
# Python integratietestvoorbeeld
@pytest.mark.asyncio
async def test_mcp_server_integration():
# Start een testserver
server = McpServer()
server.register_tool(WeatherForecastTool(MockWeatherService()))
await server.start(port=5000)
try:
# Maak een client aan
client = McpClient("http://localhost:5000")
# Test gereedschapsontdekking
tools = await client.discover_tools()
assert "weatherForecast" in [t.name for t in tools]
# Test uitvoering van hulpmiddelen
response = await client.execute_tool("weatherForecast", {
"location": "Seattle",
"days": 3
})
# Controleer reactie
assert response.status_code == 200
assert "Seattle" in response.content[0].text
assert len(json.loads(response.content[0].text)["forecasts"]) == 3
finally:
# Opruimen
await server.stop()
Prestatie Optimalisatie
1. Cache Strategieën
Implementeer passende caching om latency en resourcegebruik te verminderen:
// 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 en Testbaarheid
Ontwerp tools om hun afhankelijkheden via constructorinjectie te ontvangen, zodat ze testbaar en configureerbaar zijn:
// Java voorbeeld met dependency injection
public class CurrencyConversionTool implements Tool {
private final ExchangeRateService exchangeService;
private final CacheService cacheService;
private final Logger logger;
// Afhankelijkheden geïnjecteerd via constructor
public CurrencyConversionTool(
ExchangeRateService exchangeService,
CacheService cacheService,
Logger logger) {
this.exchangeService = exchangeService;
this.cacheService = cacheService;
this.logger = logger;
}
// Tool implementatie
// ...
}
3. Componerbare Tools
Ontwerp tools die gecombineerd kunnen worden om complexere workflows te creëren:
# Python voorbeeld dat samenstelbare hulpmiddelen laat zien
class DataFetchTool(Tool):
def get_name(self):
return "dataFetch"
# Implementatie...
class DataAnalysisTool(Tool):
def get_name(self):
return "dataAnalysis"
# Dit hulpmiddel kan resultaten van het dataFetch hulpmiddel gebruiken
async def execute_async(self, request):
# Implementatie...
pass
class DataVisualizationTool(Tool):
def get_name(self):
return "dataVisualize"
# Dit hulpmiddel kan resultaten van het dataAnalysis hulpmiddel gebruiken
async def execute_async(self, request):
# Implementatie...
pass
# Deze hulpmiddelen kunnen onafhankelijk worden gebruikt of als onderdeel van een workflow
Schema Ontwerp Beste Praktijken
Het schema is het contract tussen het model en jouw tool. Goed ontworpen schema's leiden tot betere toolbruikbaarheid.
1. Duidelijke Parameterbeschrijvingen
Voeg altijd beschrijvende informatie toe voor elke 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. Validatiebeperkingen
Voeg validatiebeperkingen toe om ongeldige invoer te voorkomen:
Map<String, Object> getSchema() {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>();
// Email-eigenschap met formaatvalidatie
Map<String, Object> email = new HashMap<>();
email.put("type", "string");
email.put("format", "email");
email.put("description", "User email address");
// Leeftijdseigenschap met numerieke beperkingen
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");
// Opgegeven eigenschap
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. Consistente Retourstructuren
Behoud consistentie in je antwoordstructuren om het voor modellen makkelijker te maken resultaten te interpreteren:
async def execute_async(self, request):
try:
# Verzoek verwerken
results = await self._search_database(request.parameters["query"])
# Altijd een consistente structuur retourneren
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
}
Foutafhandeling
Robuuste foutafhandeling is cruciaal voor MCP-tools om betrouwbaarheid te behouden.
1. Gracieze Foutafhandeling
Pak fouten af op passende niveaus en geef informatieve meldingen:
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. Gestructureerde Foutreacties
Retourneer gestructureerde foutinformatie wanneer mogelijk:
@Override
public ToolResponse execute(ToolRequest request) {
try {
// Implementatie
} 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();
}
// Gooi andere uitzonderingen opnieuw als ToolExecutionException
throw new ToolExecutionException("Tool execution failed: " + ex.getMessage(), ex);
}
}
3. Retry-Logica
Implementeer passende retry-logica bij tijdelijke fouten:
async def execute_async(self, request):
max_retries = 3
retry_count = 0
base_delay = 1 # seconden
while retry_count < max_retries:
try:
# Bel externe 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)}")
# Exponentiële 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:
# Niet-transiënte fout, niet opnieuw proberen
raise ToolExecutionException(f"Operation failed: {str(e)}")
Prestatieoptimalisatie
1. Caching
Implementeer caching voor dure operaties:
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. Asynchrone Verwerking
Gebruik asynchrone programmeerpatronen voor I/O-gebonden operaties:
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();
// Voor langlopende bewerkingen onmiddellijk een verwerkings-ID retourneren
String processId = UUID.randomUUID().toString();
// Asynchrone verwerking starten
CompletableFuture.runAsync(() -> {
try {
// Langlopende bewerking uitvoeren
documentService.processDocument(documentId);
// Status bijwerken (zou gewoonlijk in een database worden opgeslagen)
processStatusRepository.updateStatus(processId, "completed");
} catch (Exception ex) {
processStatusRepository.updateStatus(processId, "failed", ex.getMessage());
}
}, executorService);
// Onmiddellijke reactie retourneren met proces-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();
}
// Hulpmiddel voor statuscontrole
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
Implementeer resource-throttling om overbelasting te voorkomen:
class ThrottledApiTool(Tool):
def __init__(self):
self.rate_limiter = TokenBucketRateLimiter(
tokens_per_second=5, # Sta 5 verzoeken per seconde toe
bucket_size=10 # Sta pieken toe tot 10 verzoeken
)
async def execute_async(self, request):
# Controleer of we kunnen doorgaan of moeten wachten
delay = self.rate_limiter.get_delay_time()
if delay > 0:
if delay > 2.0: # Als wachten te lang duurt
raise ToolExecutionException(
f"Rate limit exceeded. Please try again in {delay:.1f} seconds."
)
else:
# Wacht de juiste vertragingstijd
await asyncio.sleep(delay)
# Verbruik een token en ga door met het verzoek
self.rate_limiter.consume()
# Roep API aan
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
# Bereken tijd tot volgende token beschikbaar is
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
# Voeg nieuwe tokens toe op basis van verstreken tijd
new_tokens = elapsed * self.tokens_per_second
self.tokens = min(self.bucket_size, self.tokens + new_tokens)
self.last_refill = now
Veiligheids Beste Praktijken
1. Invoervalidatie
Valideer invoerparameters altijd grondig:
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. Autorisatiecontroles
Implementeer correcte autorisatiecontroles:
@Override
public ToolResponse execute(ToolRequest request) {
// Haal gebruikerscontext op uit het verzoek
UserContext user = request.getContext().getUserContext();
// Controleer of de gebruiker de vereiste permissies heeft
if (!authorizationService.hasPermission(user, "documents:read")) {
throw new ToolExecutionException("User does not have permission to access documents");
}
// Controleer voor specifieke bronnen de toegang tot die bron
String documentId = request.getParameters().get("documentId").asText();
if (!documentService.canUserAccess(user.getId(), documentId)) {
throw new ToolExecutionException("Access denied to the requested document");
}
// Ga door met de uitvoering van de tool
// ...
}
3. Gevoelige Dataafhandeling
Ga voorzichtig om met gevoelige data:
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)
# Gebruikersgegevens ophalen
user_data = await self.user_service.get_user_data(user_id)
# Filter gevoelige velden tenzij expliciet aangevraagd EN geautoriseerd
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):
# Controleer het autorisatieniveau in de aanvraagcontext
auth_level = request.context.get("authorizationLevel")
return auth_level == "admin"
def _redact_sensitive_fields(self, user_data):
# Maak een kopie om wijziging van het origineel te voorkomen
redacted = user_data.copy()
# Specifieke gevoelige velden redigeren
sensitive_fields = ["ssn", "creditCardNumber", "password"]
for field in sensitive_fields:
if field in redacted:
redacted[field] = "REDACTED"
# Geneste gevoelige gegevens redigeren
if "financialInfo" in redacted:
redacted["financialInfo"] = {"available": True, "accessRestricted": True}
return redacted
Test Beste Praktijken voor MCP Tools
Uitgebreid testen zorgt ervoor dat MCP-tools correct functioneren, randgevallen afhandelen en goed integreren met de rest van het systeem.
Unit Testing
1. Test Elke Tool Geïsoleerd
Maak gerichte tests voor de functionaliteit van elke tool:
[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 Validatietests
Test dat schema's geldig zijn en beperkingen correct afdwingen:
@Test
public void testSchemaValidation() {
// Maak tool instantie aan
SearchTool searchTool = new SearchTool();
// Ontvang schema
Object schema = searchTool.getSchema();
// Converteer schema naar JSON voor validatie
String schemaJson = objectMapper.writeValueAsString(schema);
// Valideer dat schema een geldig JSONSchema is
JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
JsonSchema jsonSchema = factory.getJsonSchema(schemaJson);
// Test geldige parameters
JsonNode validParams = objectMapper.createObjectNode()
.put("query", "test query")
.put("limit", 5);
ProcessingReport validReport = jsonSchema.validate(validParams);
assertTrue(validReport.isSuccess());
// Test ontbrekende verplichte parameter
JsonNode missingRequired = objectMapper.createObjectNode()
.put("limit", 5);
ProcessingReport missingReport = jsonSchema.validate(missingRequired);
assertFalse(missingReport.isSuccess());
// Test ongeldig parametertype
JsonNode invalidType = objectMapper.createObjectNode()
.put("query", "test")
.put("limit", "not-a-number");
ProcessingReport invalidReport = jsonSchema.validate(invalidType);
assertFalse(invalidReport.isSuccess());
}
3. Foutafhandelingstests
Maak specifieke tests voor foutcondities:
@pytest.mark.asyncio
async def test_api_tool_handles_timeout():
# Ordenen
tool = ApiTool(timeout=0.1) # Zeer korte time-out
# Simuleer een verzoek dat zal time-outen
with aioresponses() as mocked:
mocked.get(
"https://api.example.com/data",
callback=lambda *args, **kwargs: asyncio.sleep(0.5) # Langer dan de time-out
)
request = ToolRequest(
tool_name="apiTool",
parameters={"url": "https://api.example.com/data"}
)
# Uitvoeren & Controleren
with pytest.raises(ToolExecutionException) as exc_info:
await tool.execute_async(request)
# Verifieer exceptiebericht
assert "timed out" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_api_tool_handles_rate_limiting():
# Ordenen
tool = ApiTool()
# Simuleer een reactie met snelheidsbeperking
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"}
)
# Uitvoeren & Controleren
with pytest.raises(ToolExecutionException) as exc_info:
await tool.execute_async(request)
# Verifieer dat de exceptie informatie over snelheidsbeperking bevat
error_msg = str(exc_info.value).lower()
assert "rate limit" in error_msg
assert "try again" in error_msg
Integratietesten
1. Toolketentest
Test tools die samenwerken in verwachte combinaties:
[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 Testen
Test de MCP-server met volledige toolregistratie en uitvoering:
@SpringBootTest
@AutoConfigureMockMvc
public class McpServerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testToolDiscovery() throws Exception {
// Test de ontdekkingsendpoint
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 {
// Maak toolverzoek aan
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);
// Verstuur verzoek en controleer antwoord
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 {
// Maak ongeldig toolverzoek aan
Map<String, Object> request = new HashMap<>();
request.put("toolName", "calculator");
Map<String, Object> parameters = new HashMap<>();
parameters.put("operation", "divide");
parameters.put("a", 10);
// Ontbrekende parameter "b"
request.put("parameters", parameters);
// Verstuur verzoek en controleer foutantwoord
mockMvc.perform(post("/mcp/execute")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").exists());
}
}
3. End-to-End Testen
Test complete workflows van modelprompt tot tooluitvoering:
@pytest.mark.asyncio
async def test_model_interaction_with_tool():
# Arrange - Stel MCP-client en mockmodel in
mcp_client = McpClient(server_url="http://localhost:5000")
# Mockmodelreacties
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=[]
)
])
# Mockweerhulpmiddelreactie
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"}
]
}
}
)
# Actie
response = await mcp_client.send_prompt(
"What's the weather in Seattle?",
model=mock_model,
allowed_tools=["weatherForecast"]
)
# Bevestigen
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"
Prestatie Testen
1. Load Testing
Test hoeveel gelijktijdige verzoeken je MCP-server aankan:
[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 het systeem onder extreme belasting:
@Test
public void testServerUnderStress() {
int maxUsers = 1000;
int rampUpTimeSeconds = 60;
int testDurationSeconds = 300;
// Stel JMeter in voor stresstesten
StandardJMeterEngine jmeter = new StandardJMeterEngine();
// Configureer JMeter testplan
HashTree testPlanTree = new HashTree();
// Maak testplan, threadgroep, 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);
// Voeg HTTP-sampler toe voor tooluitvoering
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);
// Voeg luisteraars toe
SummaryReport summaryReport = new SummaryReport();
threadGroup.add(summaryReport);
// Voer test uit
jmeter.configure(testPlanTree);
jmeter.run();
// Valideer resultaten
assertEquals(0, summaryReport.getErrorCount());
assertTrue(summaryReport.getAverage() < 200); // Gemiddelde responstijd < 200ms
assertTrue(summaryReport.getPercentile(90.0) < 500); // 90e percentiel < 500ms
}
3. Monitoring en Profiling
Zet monitoring op voor langetermijn prestatieanalyse:
# Configureer monitoring voor een MCP-server
def configure_monitoring(server):
# Stel Prometheus-metrics in
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"]
)
}
# Voeg middleware toe voor het timen en vastleggen van metrics
server.add_middleware(PrometheusMiddleware(prometheus_metrics))
# Maak metrics-endpoint beschikbaar
@server.router.get("/metrics")
async def metrics():
return generate_latest()
return server
MCP Workflow Ontwerp Patronen
Goed ontworpen MCP-workflows verbeteren efficiëntie, betrouwbaarheid en onderhoudbaarheid. Hier zijn belangrijke patronen om te volgen:
1. Ketting van Tools Patronen
Verbind meerdere tools in een reeks waarbij de output van de ene tool input wordt voor de volgende:
# Implementatie van Python Chain of Tools
class ChainWorkflow:
def __init__(self, tools_chain):
self.tools_chain = tools_chain # Lijst met gereedschapsnamen om achtereenvolgens uit te voeren
async def execute(self, mcp_client, initial_input):
current_result = initial_input
all_results = {"input": initial_input}
for tool_name in self.tools_chain:
# Voer elk gereedschap in de keten uit, waarbij het vorige resultaat wordt doorgegeven
response = await mcp_client.execute_tool(tool_name, current_result)
# Resultaat opslaan en gebruiken als invoer voor het volgende gereedschap
all_results[tool_name] = response.result
current_result = response.result
return {
"final_result": current_result,
"all_results": all_results
}
# Voorbeeld van gebruik
data_processing_chain = ChainWorkflow([
"dataFetch",
"dataCleaner",
"dataAnalyzer",
"dataVisualizer"
])
result = await data_processing_chain.execute(
mcp_client,
{"source": "sales_database", "table": "transactions"}
)
2. Dispatcher Patroon
Gebruik een centrale tool die op basis van input naar gespecialiseerde tools dispatcht:
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. Parallelle Verwerking Patroon
Voer meerdere tools gelijktijdig uit voor efficiëntie:
public class ParallelDataProcessingWorkflow {
private final McpClient mcpClient;
public ParallelDataProcessingWorkflow(McpClient mcpClient) {
this.mcpClient = mcpClient;
}
public WorkflowResult execute(String datasetId) {
// Stap 1: Haal datasetmetadata op (synchroon)
ToolResponse metadataResponse = mcpClient.executeTool("datasetMetadata",
Map.of("datasetId", datasetId));
// Stap 2: Start meerdere analyses 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"
))
);
// Wacht tot alle parallelle taken afgerond zijn
CompletableFuture<Void> allAnalyses = CompletableFuture.allOf(
statisticalAnalysis, correlationAnalysis, outlierDetection
);
allAnalyses.join(); // Wacht op voltooiing
// Stap 3: Combineer resultaten
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());
// Stap 4: Genereer samenvattend rapport
ToolResponse summaryResponse = mcpClient.executeTool("reportGenerator",
Map.of("analysisResults", combinedResults));
// Geef het volledige workflowresultaat terug
WorkflowResult result = new WorkflowResult();
result.setDatasetId(datasetId);
result.setAnalysisResults(combinedResults);
result.setSummaryReport(summaryResponse.getResult());
return result;
}
}
4. Foutherstel Patroon
Implementeer gracieze fallback-mogelijkheden voor toolfouten:
class ResilientWorkflow:
def __init__(self, mcp_client):
self.client = mcp_client
async def execute_with_fallback(self, primary_tool, fallback_tool, parameters):
try:
# Probeer eerst het primaire gereedschap
response = await self.client.execute_tool(primary_tool, parameters)
return {
"result": response.result,
"source": "primary",
"tool": primary_tool
}
except ToolExecutionException as e:
# Log de mislukking
logging.warning(f"Primary tool '{primary_tool}' failed: {str(e)}")
# Schakel over naar het secundaire gereedschap
try:
# Mogelijk parameters transformeren voor het fallback-gereedschap
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:
# Beide gereedschappen hebben gefaald
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"""
# Deze implementatie hangt af van de specifieke gereedschappen
# Voor dit voorbeeld retourneren we gewoon de originele parameters
return params
# Voorbeeldgebruik
async def get_weather(workflow, location):
return await workflow.execute_with_fallback(
"premiumWeatherService", # Primaire (betaalde) weer-API
"basicWeatherService", # Fallback (gratis) weer-API
{"location": location}
)
5. Workflow Compositie Patroon
Bouw complexe workflows door eenvoudigere samen te stellen:
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 Servers Testen: Beste Praktijken en Top Tips
Overzicht
Testen is een cruciaal aspect van het ontwikkelen van betrouwbare, hoogwaardige MCP-servers. Deze gids biedt uitgebreide beste praktijken en tips voor het testen van je MCP-servers gedurende de gehele ontwikkelingscyclus, van unittests tot integratietests en end-to-end validatie.
Waarom Testen Belangrijk Is voor MCP-Servers
MCP-servers fungeren als cruciale middleware tussen AI-modellen en clientapplicaties. Grondig testen zorgt voor:
- Betrouwbaarheid in productieomgevingen
- Nauwkeurige verwerking van verzoeken en antwoorden
- Correcte implementatie van MCP-specificaties
- Veerkracht tegen fouten en randgevallen
- Consistente prestaties onder verschillende belasting
Unit Testing voor MCP-Servers
Unit Testing (Fundament)
Unittests verifiëren individuele componenten van je MCP-server geïsoleerd.
Wat te Testen
- Resource Handlers: Test zelfstandig de logica van elke resource handler
- Toolimplementaties: Verifieer toolgedrag met diverse invoer
- Prompttemplates: Zorg dat prompttemplates correct renderen
- Schema Validatie: Test parametervalidatielogica
- Foutafhandeling: Verifieer foutreacties bij ongeldige invoer
Beste Praktijken voor 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);
}
# Voorbeeld van een unittests voor een rekenmachine-tool in Python
def test_calculator_tool_add():
# Arrangeer
calculator = CalculatorTool()
parameters = {
"operation": "add",
"a": 5,
"b": 7
}
# Voer uit
response = calculator.execute(parameters)
result = json.loads(response.content[0].text)
# Bevestig
assert result["value"] == 12
Integratietesten (Middellaag)
Integratietests verifiëren de interacties tussen componenten van je MCP-server.
Wat te Testen
- Serverinitialisatie: Test serverstart met verschillende configuraties
- Route-registratie: Verifieer dat alle endpoints correct geregistreerd zijn
- Verwerkingscyclus van Verzoeken: Test de volledige request-response cyclus
- Foutpropagering: Zorg dat fouten correct door componenten worden afgehandeld
- Authenticatie & Autorisatie: Test beveiligingsmechanismen
Beste Praktijken voor Integratietesten
// 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 Testen (Bovenlaag)
End-to-end tests verifiëren het volledige systeemgedrag van client tot server.
Wat te Testen
- Client-Server Communicatie: Test volledige request-response cycli
- Echte Client SDK's: Test met daadwerkelijke clientimplementaties
- Prestaties Onder Belast: Verifieer gedrag bij meerdere gelijktijdige verzoeken
- Foutherstel: Test systeemherstel na fouten
- Langlopende Operaties: Verifieer afhandeling van streaming en lange operaties
Beste Praktijken voor E2E Testing
// Voorbeeld E2E-test met een client in TypeScript
describe('MCP Server E2E Tests', () => {
let client: McpClient;
beforeAll(async () => {
// Start server in testomgeving
await startTestServer();
client = new McpClient('http://localhost:5000');
});
afterAll(async () => {
await stopTestServer();
});
test('Client can invoke calculator tool and get correct result', async () => {
// Actie
const response = await client.invokeToolAsync('calculator', {
operation: 'divide',
a: 20,
b: 4
});
// Bevestigen
expect(response.statusCode).toBe(200);
expect(response.content[0].text).toContain('5');
});
});
Mocking Strategieën voor MCP Testing
Mocking is essentieel om componenten te isoleren tijdens testen.
Componenten om te Mocken
- Externe AI Modellen: Mock modelantwoorden voor voorspelbare tests
- Externe Diensten: Mock API-afhankelijkheden (databases, derden services)
- Authenticatie Diensten: Mock identiteitsproviders
- Resource Providers: Mock dure resource handlers
Voorbeeld: Mocken van een AI Model Antwoord
// 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 voorbeeld met unittest.mock
@patch('mcp_server.models.OpenAIModel')
def test_with_mock_model(mock_model):
# Stel mock in
mock_model.return_value.generate_response.return_value = {
"text": "Mocked model response",
"finish_reason": "completed"
}
# Gebruik mock in test
server = McpServer(model_client=mock_model)
# Ga door met test
Prestatie Testen
Prestatie testen is cruciaal voor productie MCP-servers.
Wat te Meten
- Latentie: Reactietijd van verzoeken
- Doorvoer: Verzoeken per seconde verwerkt
- Resourcegebruik: CPU-, geheugen- en netwerkgebruik
- Gelijktijdigheidsafhandeling: Gedrag onder parallelle verzoeken
- Schaalariemen: Prestatie naarmate de belasting toeneemt
Tools voor Prestatie Testen
- k6: Open-source load testtool
- JMeter: Uitgebreide prestatie testtool
- Locust: Python-gebaseerde load testing
- Azure Load Testing: Cloud-gebaseerde prestatie testing
Voorbeeld: Basistest Load met k6
// k6-script voor loadtesten van MCP-server
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 virtuele gebruikers
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);
}
Testautomatisering voor MCP-Servers
Automatisering van je tests zorgt voor consistente kwaliteit en snellere feedbackloops.
CI/CD Integratie
- Voer Unit Tests uit bij Pull Requests: Zorg dat codewijzigingen bestaande functionaliteit niet breken
- Integratietests in Staging: Voer integratietests uit in pre-productieomgevingen
- Prestatiebasislijnen: Onderhoud prestatienormen om regressies te detecteren
- Beveiligingsscans: Automatiseer beveiligingstests als onderdeel van de pipeline
Voorbeeld 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
Testen op Naleving van MCP Specificatie
Controleer of je server de MCP-specificatie correct implementeert.
Belangrijke Nalevingsgebieden
- API-eindpunten: Test vereiste eindpunten (/resources, /tools, enz.)
- Aanvraag/Reactieformaat: Valideer schema-naleving
- Foutcodes: Controleer correcte statuscodes voor verschillende scenario’s
- Contenttypen: Test de afhandeling van verschillende contenttypen
- Authenticatiestroom: Controleer spec-conforme authenticatiemechanismen
Naleving 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 voor Effectief MCP Server Testen
- Test Tooldefinities Apart: Controleer schema-definities onafhankelijk van toollogica
- Gebruik Geparametriseerde Tests: Test tools met diverse invoer, inclusief randgevallen
- Controleer Foutreacties: Verifieer correcte foutafhandeling voor alle mogelijke foutcondities
- Test Autorisatielogica: Zorg voor juiste toegangscontrole voor verschillende gebruikersrollen
- Monitor Testdekking: Streef naar hoge dekking van kritieke codepaden
- Test Streaming Reacties: Verifieer correcte afhandeling van streaming content
- Simuleer Netwerkproblemen: Test gedrag onder slechte netwerkcondities
- Test Resourcebeperkingen: Controleer gedrag bij het bereiken van quota of snelheidslimieten
- Automatiseer Regressietests: Bouw een suite die bij elke codewijziging draait
- Documenteer Testcases: Houd duidelijke documentatie bij van testsituaties
Veelvoorkomende Testvalkuilen
- Te veel vertrouwen op 'happy path' testen: Zorg ervoor dat foutgevallen grondig getest worden
- Negeren van prestatietests: Identificeer knelpunten voordat ze productie beïnvloeden
- Alleen isolatietesten uitvoeren: Combineer unit-, integratie- en end-to-end testen
- Onvolledige API-dekking: Zorg dat alle eindpunten en functies getest worden
- Inconsistente testomgevingen: Gebruik containers om consistente testomgevingen te garanderen
Conclusie
Een uitgebreide teststrategie is essentieel voor het ontwikkelen van betrouwbare, hoogwaardige MCP-servers. Door de beste praktijken en tips in deze gids toe te passen, kun je ervoor zorgen dat je MCP-implementaties voldoen aan de hoogste kwaliteitseisen op het gebied van betrouwbaarheid en prestaties.
Belangrijke Leerpunten
- Toolontwerp: Volg het single responsibility-principe, gebruik dependency injection en ontwerp voor composeerbaarheid
- Schemadesign: Maak duidelijke, goed gedocumenteerde schema's met de juiste validatiebeperkingen
- Foutafhandeling: Implementeer nette foutafhandeling, gestructureerde foutreacties en retry-logica
- Prestaties: Gebruik caching, asynchrone verwerking en resource-throttling
- Beveiliging: Pas grondige invoervalidatie, autorisatiecontroles en gevoelige gegevensverwerking toe
- Testen: Maak uitgebreide unit-, integratie- en end-to-end tests
- Workflowpatronen: Pas gevestigde patronen toe zoals ketens, dispatchers en parallelle verwerking
Oefening
Ontwerp een MCP-tool en workflow voor een documentverwerkingssysteem dat:
- Documenten accepteert in meerdere formaten (PDF, DOCX, TXT)
- Tekst en belangrijke informatie uit de documenten extraheert
- Documenten classificeert op type en inhoud
- Een samenvatting van elk document genereert
Implementeer de toolschema's, foutafhandeling en een workflowpatroon dat het beste bij dit scenario past. Overweeg hoe je deze implementatie zou testen.
Bronnen
- Word lid van de MCP-community op de Microsoft Foundry Discord Community om op de hoogte te blijven van de nieuwste ontwikkelingen
- Draag bij aan open-source MCP-projecten
- Pas MCP-principes toe in de AI-initiatieven van je eigen organisatie
- Verken gespecialiseerde MCP-implementaties voor jouw sector
- Overweeg het volgen van gevorderde cursussen over specifieke MCP-onderwerpen, zoals multi-modale integratie of enterprise applicatie-integratie
- Experimenteer met het bouwen van je eigen MCP-tools en workflows met de principes geleerd via de Hands on Lab
Wat Nu?
Volgende: Case Studies
Disclaimer: Dit document is vertaald met behulp van de AI vertaaldienst Co-op Translator. Hoewel we streven naar nauwkeurigheid, dient u er rekening mee te houden dat geautomatiseerde vertalingen fouten of onnauwkeurigheden kunnen bevatten. Het originele document in de oorspronkelijke taal moet worden beschouwd als de gezaghebbende bron. Voor kritieke informatie wordt professionele menselijke vertaling aanbevolen. Wij zijn niet aansprakelijk voor eventuele misverstanden of verkeerde interpretaties die voortvloeien uit het gebruik van deze vertaling.
