Java implementation of the Model Context Protocol (MCP) client for the LangChain4j framework, enabling integration with MCP servers for tools, resources, and prompts
The logging and listeners package provides interfaces and implementations for monitoring MCP client operations and handling log messages from MCP servers.
// Log message handling
McpLogMessageHandler logHandler = message -> {
System.out.println("[" + message.level() + "] " + message.data());
};
McpClient client = DefaultMcpClient.builder()
.transport(transport)
.logHandler(logHandler) // or new DefaultMcpLogMessageHandler()
.build();
// Client lifecycle monitoring
McpClientListener listener = new McpClientListener() {
@Override
public void beforeExecuteTool(McpCallContext context) {
System.out.println("Executing tool...");
}
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
System.out.println("Tool completed");
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
System.err.println("Tool failed: " + error.getMessage());
}
};
McpClient client = DefaultMcpClient.builder()
.transport(transport)
.listener(listener)
.build();
// Context-aware headers
McpHeadersSupplier headersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getToken());
return headers;
};// Logging
import dev.langchain4j.mcp.client.logging.McpLogLevel;
import dev.langchain4j.mcp.client.logging.McpLogMessage;
import dev.langchain4j.mcp.client.logging.McpLogMessageHandler;
import dev.langchain4j.mcp.client.logging.DefaultMcpLogMessageHandler;
// Listeners
import dev.langchain4j.mcp.client.McpClientListener;
import dev.langchain4j.mcp.client.McpCallContext;
import dev.langchain4j.mcp.client.McpHeadersSupplier;
// Related types
import dev.langchain4j.service.tool.ToolExecutionResult;
import dev.langchain4j.invocation.InvocationContext;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;Enumeration of log levels for MCP server log messages, following standard syslog severity levels.
enum McpLogLevel {
DEBUG, // Debug-level messages
INFO, // Informational messages
NOTICE, // Normal but significant conditions
WARNING, // Warning messages
ERROR, // Error conditions
CRITICAL, // Critical conditions
ALERT, // Action must be taken immediately
EMERGENCY; // System is unusable
static McpLogLevel from(String val);
}Severity Order (lowest to highest): DEBUG < INFO < NOTICE < WARNING < ERROR < CRITICAL < ALERT < EMERGENCY
Parses a log level from a string (case-insensitive).
Parameters: val (String) - Log level string
Returns: McpLogLevel or null if unknown
Thread-safe: Yes
Represents a log message received from an MCP server.
record McpLogMessage(McpLogLevel level, String logger, JsonNode data) {
static McpLogMessage fromJson(JsonNode json);
}Constructor Parameters:
level (McpLogLevel, required, never null) - Log severity levellogger (String, nullable) - Logger namedata (JsonNode, nullable) - Log message dataMethods: level(), logger(), data(), fromJson(JsonNode)
Parses an MCP log message from the JSON params object inside a notifications/message message.
Parameters: json (JsonNode, required) - JSON params object
Returns: McpLogMessage
Thread-safe: Yes
Interface for handling log messages received from MCP servers.
@FunctionalInterface
interface McpLogMessageHandler {
void handleLogMessage(McpLogMessage message);
}Called when a log message is received from an MCP server.
Parameters: message (McpLogMessage, required) - The log message to handle
Thread-safe: Implementation-dependent
Called from: Transport thread (async)
Use Cases:
Default implementation that forwards MCP log messages to SLF4J logger with appropriate severity mapping.
class DefaultMcpLogMessageHandler implements McpLogMessageHandler {
void handleLogMessage(McpLogMessage message);
}Level Mapping:
DEBUG → logger.debug()INFO, NOTICE → logger.info()WARNING → logger.warn()ERROR, CRITICAL, ALERT, EMERGENCY → logger.error()Log Format: "MCP logger: {loggerName}: {data}"
Example Output:
[DEBUG] MCP logger: server-logger: {"message": "Processing request"}
[INFO] MCP logger: server-logger: {"message": "Request completed"}
[ERROR] MCP logger: server-logger: {"message": "Connection failed", "error": "timeout"}// Filter and format log messages
McpLogMessageHandler customHandler = message -> {
// Only handle error-level logs
if (message.level() == McpLogLevel.ERROR ||
message.level() == McpLogLevel.CRITICAL ||
message.level() == McpLogLevel.EMERGENCY) {
System.err.println("MCP Server Error!");
System.err.println("Logger: " + message.logger());
System.err.println("Level: " + message.level());
System.err.println("Data: " + message.data());
// Send alert to monitoring system
alertSystem.sendAlert(
"MCP Error",
message.logger(),
message.data().toString()
);
}
};
McpClient client = DefaultMcpClient.builder()
.transport(transport)
.logHandler(customHandler)
.build();// Only log messages at or above configured level
McpLogLevel minLevel = McpLogLevel.WARNING; // From config
McpLogMessageHandler levelFilterHandler = message -> {
if (message.level().ordinal() >= minLevel.ordinal()) {
System.out.println("[" + message.level() + "] " +
message.logger() + ": " + message.data());
}
};import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
McpLogMessageHandler structuredHandler = message -> {
try {
Map<String, Object> logEntry = Map.of(
"timestamp", Instant.now().toString(),
"source", "mcp-server",
"level", message.level().toString(),
"logger", message.logger(),
"data", message.data()
);
String json = mapper.writeValueAsString(logEntry);
System.out.println(json);
} catch (Exception e) {
System.err.println("Failed to log message: " + e.getMessage());
}
};Listener interface for monitoring MCP client operations including tool execution, resource retrieval, and prompt retrieval.
interface McpClientListener {
// Tool execution
void beforeExecuteTool(McpCallContext context);
void afterExecuteTool(McpCallContext context, ToolExecutionResult result, Map<String, Object> rawResult);
void onExecuteToolError(McpCallContext context, Throwable error);
// Resource operations
void beforeResourceGet(McpCallContext context);
void afterResourceGet(McpCallContext context, McpReadResourceResult result, Map<String, Object> rawResult);
void onResourceGetError(McpCallContext context, Throwable error);
// Prompt operations
void beforePromptGet(McpCallContext context);
void afterPromptGet(McpCallContext context, McpGetPromptResult result, Map<String, Object> rawResult);
void onPromptGetError(McpCallContext context, Throwable error);
}All methods have default empty implementations - implement only the methods you need.
Thread Safety: Methods called synchronously in the calling thread Performance: Keep implementations lightweight to avoid blocking operations
Called before executing a tool on the MCP server.
Parameters: context (McpCallContext, required) - Call context including invocation context and message
Called from: Same thread as executeTool() call
Use Cases: Logging, metrics start, authorization checks
Called after successfully executing a tool, or if it resulted in an application-level error (but not a protocol-level or communication error).
Parameters:
context (McpCallContext, required) - Call contextresult (ToolExecutionResult, required) - Tool execution resultrawResult (Map<String, Object>, required) - Raw JSON-RPC result from serverCalled from: Same thread as executeTool() call
Use Cases: Logging, metrics end, audit trail, result validation
Note: Called even if tool returned an error result (application error), but NOT called for protocol errors (those trigger onExecuteToolError).
Called when tool execution fails due to a protocol-level or communication error.
Parameters:
context (McpCallContext, required) - Call contexterror (Throwable, required) - The error that occurredCalled from: Same thread as executeTool() call
Error Types: McpException, IllegalResponseException, network errors, timeouts
Use Cases: Error logging, error metrics, alerting
Called before retrieving a resource from the MCP server.
Parameters: context (McpCallContext, required)
Called from: Same thread as readResource() call
Called after successfully retrieving a resource.
Parameters:
context (McpCallContext, required)result (McpReadResourceResult, required) - Resource read resultrawResult (Map<String, Object>, required) - Raw JSON-RPC resultCalled from: Same thread as readResource() call
Called when resource retrieval fails.
Parameters:
context (McpCallContext, required)error (Throwable, required)Called from: Same thread as readResource() call
Called before retrieving a prompt from the MCP server.
Parameters: context (McpCallContext, required)
Called from: Same thread as getPrompt() call
Called after successfully retrieving a prompt.
Parameters:
context (McpCallContext, required)result (McpGetPromptResult, required) - Prompt get resultrawResult (Map<String, Object>, required) - Raw JSON-RPC resultCalled from: Same thread as getPrompt() call
Called when prompt retrieval fails.
Parameters:
context (McpCallContext, required)error (Throwable, required)Called from: Same thread as getPrompt() call
class PerformanceMonitoringListener implements McpClientListener {
private final Map<String, Instant> startTimes = new ConcurrentHashMap<>();
@Override
public void beforeExecuteTool(McpCallContext context) {
String callId = getCallId(context);
startTimes.put(callId, Instant.now());
metrics.increment("tool.executions.started");
}
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
recordDuration("tool", context);
metrics.increment("tool.executions.succeeded");
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
recordDuration("tool", context);
metrics.increment("tool.executions.failed");
}
private void recordDuration(String operation, McpCallContext context) {
String callId = getCallId(context);
Instant start = startTimes.remove(callId);
if (start != null) {
Duration duration = Duration.between(start, Instant.now());
metrics.recordDuration(operation, duration);
logger.info("{} took {}ms", operation, duration.toMillis());
}
}
private String getCallId(McpCallContext context) {
return context.message().toString();
}
}class TracingListener implements McpClientListener {
private final Tracer tracer;
@Override
public void beforeExecuteTool(McpCallContext context) {
InvocationContext invocationContext = context.invocationContext();
if (invocationContext != null) {
String traceId = (String) invocationContext.attributes().get("traceId");
String toolName = extractToolName(context.message());
tracer.startSpan("mcp.tool.execute")
.tag("trace.id", traceId)
.tag("tool.name", toolName);
}
}
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
tracer.endSpan();
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
tracer.recordError(error);
tracer.endSpan();
}
private String extractToolName(Object message) {
// Extract tool name from message
return "unknown";
}
}// Implement only error methods
McpClientListener errorListener = new McpClientListener() {
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
errorTracker.logError("Tool execution failed", error, Map.of(
"context", context.toString()
));
}
@Override
public void onResourceGetError(McpCallContext context, Throwable error) {
errorTracker.logError("Resource retrieval failed", error);
}
@Override
public void onPromptGetError(McpCallContext context, Throwable error) {
errorTracker.logError("Prompt retrieval failed", error);
}
// Other methods use default (empty) implementation
};class AuditLoggingListener implements McpClientListener {
private final AuditLogger auditLogger;
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
auditLogger.log(
"TOOL_EXECUTED",
Map.of(
"user", getUserFromContext(context),
"tool", extractToolName(context),
"success", true,
"timestamp", Instant.now()
)
);
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
auditLogger.log(
"TOOL_EXECUTION_FAILED",
Map.of(
"user", getUserFromContext(context),
"tool", extractToolName(context),
"error", error.getMessage(),
"success", false,
"timestamp", Instant.now()
)
);
}
private String getUserFromContext(McpCallContext context) {
InvocationContext invocationContext = context.invocationContext();
if (invocationContext != null) {
Object userId = invocationContext.attributes().get("userId");
return userId != null ? userId.toString() : "unknown";
}
return "unknown";
}
private String extractToolName(McpCallContext context) {
// Implementation depends on message structure
return "unknown";
}
}class RateLimitedErrorListener implements McpClientListener {
private final Duration minInterval = Duration.ofSeconds(5);
private final AtomicReference<Instant> lastNotification =
new AtomicReference<>(Instant.MIN);
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
notifyIfReady("Tool execution error: " + error.getMessage());
}
@Override
public void onResourceGetError(McpCallContext context, Throwable error) {
notifyIfReady("Resource retrieval error: " + error.getMessage());
}
@Override
public void onPromptGetError(McpCallContext context, Throwable error) {
notifyIfReady("Prompt retrieval error: " + error.getMessage());
}
private void notifyIfReady(String message) {
Instant now = Instant.now();
Instant last = lastNotification.get();
if (Duration.between(last, now).compareTo(minInterval) > 0) {
if (lastNotification.compareAndSet(last, now)) {
notificationService.send(message);
}
}
}
}Functional interface for supplying dynamic HTTP headers for MCP requests based on the call context.
@FunctionalInterface
interface McpHeadersSupplier extends Function<McpCallContext, Map<String, String>> {
Map<String, String> apply(McpCallContext context);
}Use Cases:
Thread-safe: Implementation-dependent Called from: Transport thread before each request
McpHeadersSupplier headersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getToken());
return headers;
};McpHeadersSupplier headersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
// Automatically refresh token if needed
String token = tokenManager.getValidToken();
headers.put("Authorization", "Bearer " + token);
return headers;
};McpHeadersSupplier headersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
// Always include authentication
headers.put("Authorization", "Bearer " + getToken());
// Add context-specific headers if available
InvocationContext invocationContext = context.invocationContext();
if (invocationContext != null) {
// Add user ID
Object userId = invocationContext.attributes().get("userId");
if (userId != null) {
headers.put("X-User-ID", userId.toString());
}
// Add session ID
Object sessionId = invocationContext.attributes().get("sessionId");
if (sessionId != null) {
headers.put("X-Session-ID", sessionId.toString());
}
// Add request ID for tracing
Object requestId = invocationContext.attributes().get("requestId");
if (requestId != null) {
headers.put("X-Request-ID", requestId.toString());
}
}
return headers;
};
// Use with HTTP or WebSocket transport
StreamableHttpMcpTransport transport = StreamableHttpMcpTransport.builder()
.url("https://mcp-server.example.com")
.customHeaders(headersSupplier)
.build();McpHeadersSupplier tracingHeadersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
InvocationContext invocationContext = context.invocationContext();
if (invocationContext != null) {
// Propagate trace context
String traceId = (String) invocationContext.attributes().get("traceId");
String spanId = (String) invocationContext.attributes().get("spanId");
if (traceId != null && spanId != null) {
headers.put("X-Trace-ID", traceId);
headers.put("X-Span-ID", spanId);
headers.put("X-Parent-Span-ID", spanId);
}
}
return headers;
};// Log handler
McpLogMessageHandler logHandler = new DefaultMcpLogMessageHandler();
// Client listener
McpClientListener listener = new McpClientListener() {
@Override
public void beforeExecuteTool(McpCallContext context) {
logger.info("Executing tool");
metrics.increment("tool.executions");
}
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
logger.info("Tool executed successfully");
metrics.increment("tool.executions.success");
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
logger.error("Tool execution failed", error);
metrics.increment("tool.executions.failure");
}
};
// Headers supplier
McpHeadersSupplier headersSupplier = context -> {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getToken());
InvocationContext invocationContext = context.invocationContext();
if (invocationContext != null) {
Object userId = invocationContext.attributes().get("userId");
if (userId != null) {
headers.put("X-User-ID", userId.toString());
}
}
return headers;
};
// Create transport
StreamableHttpMcpTransport transport = StreamableHttpMcpTransport.builder()
.url("https://mcp-server.example.com")
.customHeaders(headersSupplier)
.build();
// Create client with all monitoring
McpClient client = DefaultMcpClient.builder()
.transport(transport)
.logHandler(logHandler)
.listener(listener)
.build();class CompositeListener implements McpClientListener {
private final List<McpClientListener> listeners;
CompositeListener(McpClientListener... listeners) {
this.listeners = Arrays.asList(listeners);
}
@Override
public void beforeExecuteTool(McpCallContext context) {
listeners.forEach(l -> l.beforeExecuteTool(context));
}
@Override
public void afterExecuteTool(McpCallContext context,
ToolExecutionResult result,
Map<String, Object> rawResult) {
listeners.forEach(l -> l.afterExecuteTool(context, result, rawResult));
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
listeners.forEach(l -> l.onExecuteToolError(context, error));
}
// Implement other methods similarly
}
// Use composite listener
McpClient client = DefaultMcpClient.builder()
.transport(transport)
.listener(new CompositeListener(
new PerformanceMonitoringListener(),
new ErrorTrackingListener(),
new AuditLoggingListener()
))
.build();All implementations should be thread-safe if the client is used from multiple threads.
Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-mcp