McpClientFactory.java
package de.mirkosertic.powerstaff.profilesearch.command;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import tools.jackson.databind.json.JsonMapper;
import java.net.http.HttpClient;
/**
* Factory für die Erstellung von MCP (Model Context Protocol) Client-Instanzen.
* <p>
* Erstellt pro Request eine neue MCP-Session mit automatischer Retry-Logik bei
* Connection-Fehlern. Die Konfiguration erfolgt via {@link McpConnectionProperties}.
* <p>
* <strong>Retry-Verhalten:</strong>
* <ul>
* <li>Bei Connection-Fehler: maxRetries Versuche mit retryDelay Pause dazwischen</li>
* <li>DEBUG-Log bei jedem Retry-Versuch</li>
* <li>McpConnectionException nach allen fehlgeschlagenen Versuchen</li>
* </ul>
*
* @see McpConnectionProperties
* @see McpConnectionException
*/
@Component
public class McpClientFactory {
private static final Logger logger = LoggerFactory.getLogger(McpClientFactory.class);
private final McpConnectionProperties properties;
public McpClientFactory(final McpConnectionProperties properties) {
this.properties = properties;
}
/**
* Erstellt einen neuen MCP-Client mit Retry-Logik.
* <p>
* Die Methode versucht maxRetries+1 mal eine Verbindung aufzubauen.
* Zwischen den Versuchen wird retryDelay gewartet.
*
* @return initialisierter McpSyncClient
* @throws McpConnectionException wenn MCP deaktiviert ist oder alle Verbindungsversuche fehlschlagen
*/
public McpSyncClient createClient() throws McpConnectionException {
if (!properties.isEnabled()) {
throw new McpConnectionException("MCP ist deaktiviert (enabled=false)");
}
int attempt = 0;
Exception lastException = null;
final int totalAttempts = properties.getMaxRetries() + 1;
while (attempt < totalAttempts) {
try {
final McpSyncClient client = buildClient();
logger.debug("MCP Client erfolgreich erstellt (Versuch {}/{})", attempt + 1, totalAttempts);
return client;
} catch (final Exception e) {
lastException = e;
attempt++;
if (attempt < totalAttempts) {
final long delayMs = properties.getRetryDelay().toMillis();
logger.debug("MCP Verbindung fehlgeschlagen (Versuch {}/{}), retry in {}ms: {}",
attempt, totalAttempts, delayMs, e.getMessage());
sleep(properties.getRetryDelay());
}
}
}
// Alle Versuche fehlgeschlagen
logger.error("MCP Verbindung nach {} Versuchen fehlgeschlagen", totalAttempts, lastException);
throw new McpConnectionException(
String.format("MCP Verbindung nach %d Versuchen fehlgeschlagen", totalAttempts),
lastException
);
}
/**
* Baut den MCP-Client mit HTTP-Streaming-Transport.
* Verwendet Spring AI's Builder-API für HttpClientStreamableHttpTransport.
* <p>
* Wichtig: URL und Endpoint müssen separat übergeben werden:
* <ul>
* <li>builder(url) - nur Base-URL (Schema + Host + Port)</li>
* <li>.endpoint(path) - Pfad zum MCP-Endpoint</li>
* </ul>
*/
private McpSyncClient buildClient() {
// 1. JSON Mapper erstellen
final JsonMapper jsonMapper = JsonMapper.builder().build();
// 2. Transport aufbauen (URL und Endpoint separat!)
final HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(properties.getUrl())
.endpoint(properties.getEndpoint())
.clientBuilder(HttpClient.newBuilder()
.connectTimeout(properties.getRequestTimeout()))
.jsonMapper(new JacksonMcpJsonMapper(jsonMapper))
.build();
// 3. Client-Info
final McpSchema.Implementation clientInfo = new McpSchema.Implementation(
"powerstaff-profilesearch",
"1.0.0"
);
// 4. McpSyncClient aufbauen
return McpClient.sync(transport)
.clientInfo(clientInfo)
.requestTimeout(properties.getRequestTimeout())
.build();
}
/**
* Sleep-Wrapper für Retry-Delay.
*/
private void sleep(final java.time.Duration duration) {
try {
Thread.sleep(duration.toMillis());
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Retry-Sleep wurde unterbrochen", e);
}
}
}