CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-mcp

Java implementation of the Model Context Protocol (MCP) client for the LangChain4j framework, enabling integration with MCP servers for tools, resources, and prompts

Overview
Eval results
Files

logging-listeners.mddocs/

Logging and Listeners

The logging and listeners package provides interfaces and implementations for monitoring MCP client operations and handling log messages from MCP servers.

Quick Reference

// 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;
};

Imports

// 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;

Logging

McpLogLevel

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

from(String)

Parses a log level from a string (case-insensitive).

Parameters: val (String) - Log level string Returns: McpLogLevel or null if unknown Thread-safe: Yes

McpLogMessage

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 level
  • logger (String, nullable) - Logger name
  • data (JsonNode, nullable) - Log message data

Methods: level(), logger(), data(), fromJson(JsonNode)

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

McpLogMessageHandler

Interface for handling log messages received from MCP servers.

@FunctionalInterface
interface McpLogMessageHandler {
    void handleLogMessage(McpLogMessage message);
}

handleLogMessage(McpLogMessage)

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:

  • Forward logs to centralized logging system
  • Filter logs by level
  • Trigger alerts on errors
  • Collect metrics from logs

DefaultMcpLogMessageHandler

Default implementation that forwards MCP log messages to SLF4J logger with appropriate severity mapping.

class DefaultMcpLogMessageHandler implements McpLogMessageHandler {
    void handleLogMessage(McpLogMessage message);
}

Level Mapping:

  • DEBUGlogger.debug()
  • INFO, NOTICElogger.info()
  • WARNINGlogger.warn()
  • ERROR, CRITICAL, ALERT, EMERGENCYlogger.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"}

Usage Examples

Custom Log Message Handler

// 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();

Level-Based Filtering

// 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());
    }
};

Structured Logging

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());
    }
};

Client Listeners

McpClientListener

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

Methods

beforeExecuteTool(McpCallContext)

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

afterExecuteTool(McpCallContext, ToolExecutionResult, Map)

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 context
  • result (ToolExecutionResult, required) - Tool execution result
  • rawResult (Map<String, Object>, required) - Raw JSON-RPC result from server

Called 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).

onExecuteToolError(McpCallContext, Throwable)

Called when tool execution fails due to a protocol-level or communication error.

Parameters:

  • context (McpCallContext, required) - Call context
  • error (Throwable, required) - The error that occurred

Called from: Same thread as executeTool() call Error Types: McpException, IllegalResponseException, network errors, timeouts Use Cases: Error logging, error metrics, alerting

beforeResourceGet(McpCallContext)

Called before retrieving a resource from the MCP server.

Parameters: context (McpCallContext, required) Called from: Same thread as readResource() call

afterResourceGet(McpCallContext, McpReadResourceResult, Map)

Called after successfully retrieving a resource.

Parameters:

  • context (McpCallContext, required)
  • result (McpReadResourceResult, required) - Resource read result
  • rawResult (Map<String, Object>, required) - Raw JSON-RPC result

Called from: Same thread as readResource() call

onResourceGetError(McpCallContext, Throwable)

Called when resource retrieval fails.

Parameters:

  • context (McpCallContext, required)
  • error (Throwable, required)

Called from: Same thread as readResource() call

beforePromptGet(McpCallContext)

Called before retrieving a prompt from the MCP server.

Parameters: context (McpCallContext, required) Called from: Same thread as getPrompt() call

afterPromptGet(McpCallContext, McpGetPromptResult, Map)

Called after successfully retrieving a prompt.

Parameters:

  • context (McpCallContext, required)
  • result (McpGetPromptResult, required) - Prompt get result
  • rawResult (Map<String, Object>, required) - Raw JSON-RPC result

Called from: Same thread as getPrompt() call

onPromptGetError(McpCallContext, Throwable)

Called when prompt retrieval fails.

Parameters:

  • context (McpCallContext, required)
  • error (Throwable, required)

Called from: Same thread as getPrompt() call

Usage Examples

Performance Monitoring Listener

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();
    }
}

Distributed Tracing Listener

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";
    }
}

Selective Listener (Only Errors)

// 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
};

Audit Logging Listener

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";
    }
}

Rate-Limited Error Listener

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);
            }
        }
    }
}

Headers Suppliers

McpHeadersSupplier

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:

  • Dynamic authentication (token refresh)
  • Context-aware headers (session ID, trace ID)
  • Request-specific metadata
  • Distributed tracing propagation

Thread-safe: Implementation-dependent Called from: Transport thread before each request

Usage Examples

Basic Authentication

McpHeadersSupplier headersSupplier = context -> {
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + getToken());
    return headers;
};

Token Refresh

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;
};

Context-Aware 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();

Distributed Tracing Propagation

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;
};

Complete Examples

Comprehensive Monitoring Setup

// 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();

Multiple Listeners (Composite Pattern)

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();

Performance Considerations

  1. Keep implementations lightweight: Listeners are called synchronously
  2. Avoid blocking operations: Don't do network calls or heavy computation
  3. Use async for expensive operations: Delegate to background threads
  4. Minimize allocations: Reuse objects where possible
  5. Handle exceptions: Uncaught exceptions may break the call chain

Thread Safety

  • Log handlers: Called from transport threads (async)
  • Client listeners: Called from the same thread as client methods (sync)
  • Headers suppliers: Called from transport threads before requests

All implementations should be thread-safe if the client is used from multiple threads.

Related Documentation

  • Client - Client methods that trigger listener events
  • Exceptions - Error types passed to error handlers

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-mcp

docs

client.md

data-models.md

exceptions.md

index.md

logging-listeners.md

registry.md

resources-as-tools.md

tool-provider.md

transports.md

tile.json