Enterprise Java

MCP Logging in Spring AI

The Model Context Protocol (MCP) lets an AI application call out to external tools and data sources through a standard client-server interface. When a tool runs on the server, the client usually only sees the final result — which makes it hard to know what happened in between: which step the server reached, what branch it took, or why it took so long. MCP solves this with a built-in logging capability: the server can stream log messages to the client while a tool is executing, independent of the tool’s actual return value. Spring AI’s MCP support exposes this capability on both the server and client side with just a few lines of code. This guide covers what MCP and MCP logging are, then walks through one complete example of a server that logs tool execution and a client that consumes those logs.

1. What Is MCP? What Is MCP Logging?

MCP (Model Context Protocol) is an open protocol that standardizes how an AI application (the client) talks to external systems (the server) that expose tools, resources, and prompts. Instead of writing a bespoke integration for every tool, both sides speak the same protocol — the client discovers what the server offers and invokes it in a uniform way, over transports like stdio or HTTP/SSE. MCP logging is one of the optional capabilities a server can declare. Once enabled, the server can send notifications/message events to the client at any point — before, during, or after a tool call — each carrying a severity level (debug, info, notice, warning, error, critical, alert, emergency), a logger name, and a message. The client can also call logging/setLevel to tell the server the minimum severity it wants to receive, filtering out noise like debug messages in production.

  • Logging is separate from the tool’s return value — it’s for observability, not for passing data back to the model.
  • It’s capability-based — a server only sends log notifications if it declared logging support during initialization.
  • It’s level-filtered — the client controls the noise floor via setLoggingLevel(...).

2. Code Example

The example below has two parts: an MCP server that exposes a single weather tool and emits log notifications while handling it, and an MCP client that subscribes to those logs and calls the tool.

2.1 Dependencies and Configuration

Spring AI ships separate starters for the MCP server and MCP client; the server also needs the logging capability turned on explicitly in configuration.

<!-- server -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

<!-- client -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
# application.properties (server)
spring.ai.mcp.server.name=weather-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.capabilities.logging=true

The spring-ai-starter-mcp-server-webmvc starter auto-configures an MCP server over HTTP/SSE, while spring.ai.mcp.server.capabilities.logging=true is what actually advertises the logging capability to connecting clients — without it, calls to exchange.loggingNotification(...) on the server are simply ignored.

2.2 Server: Tool That Emits Log Notifications

A regular @Tool-annotated method can accept an McpSyncServerExchange parameter, which Spring AI injects automatically and which is used to push log messages back to the client while the tool runs.

package com.example.mcp;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;

import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;

@Service
public class WeatherTools {

    @Tool(description = "Get the current weather for a city")
    public String getWeather(String city, McpSyncServerExchange exchange) {
        exchange.loggingNotification(LoggingMessageNotification.builder()
                .level(LoggingLevel.DEBUG)
                .logger("weather-tool")
                .data("Received weather request for city: " + city)
                .build());

        String result = lookupWeather(city);

        exchange.loggingNotification(LoggingMessageNotification.builder()
                .level(LoggingLevel.INFO)
                .logger("weather-tool")
                .data("Weather lookup completed for city: " + city)
                .build());

        return result;
    }

    private String lookupWeather(String city) {
        return "Sunny, 25°C in " + city;
    }
}

WeatherTools is a complete, self-contained @Service class: the imports pull in @Tool and @Service from Spring AI, plus McpSyncServerExchange, LoggingLevel, and LoggingMessageNotification from the MCP Java SDK that the method actually uses. Inside getWeather, each call to exchange.loggingNotification(...) builds a LoggingMessageNotification with a severity level, a logger name used to identify the source, and a free-text message, then sends it to the client immediately — the first notification fires before the lookup happens and the second fires right after, giving the client visibility into both the start and the end of the operation without changing what getWeather returns.

2.3 Server: Registering the Tool

The MCP server auto-configuration discovers tools through a ToolCallbackProvider bean, so WeatherTools needs to be wrapped and exposed that way.

package com.example.mcp;

import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class McpServerConfig {

    @Bean
    public ToolCallbackProvider weatherTools(WeatherTools weatherTools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(weatherTools)
                .build();
    }
}

McpServerConfig is a standalone @Configuration class whose imports bring in just the four Spring types the weatherTools bean needs: @Configuration and @Bean to declare the bean, plus ToolCallbackProvider and MethodToolCallbackProvider to build it. MethodToolCallbackProvider scans the given object (here, the WeatherTools bean injected as a method parameter) for @Tool-annotated methods and publishes them as callbacks the MCP server can list and invoke; this is the same mechanism Spring AI uses to expose tools to a chat model, reused here to expose them over MCP.

2.4 Client: Consuming Log Notifications

On the client side, a loggingConsumer is registered when building the McpSyncClient, and setLoggingLevel(...) sets the minimum severity the server should send.

package com.example.mcp;

import java.util.Map;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;

public class WeatherClient {

    public static void main(String[] args) {
        var transport = HttpClientSseClientTransport.builder("http://localhost:8080").build();

        McpSyncClient client = McpClient.sync(transport)
                .loggingConsumer(notification ->
                        System.out.printf("[%s] %s: %s%n",
                                notification.level(), notification.logger(), notification.data()))
                .build();

        client.initialize();
        client.setLoggingLevel(LoggingLevel.DEBUG);

        CallToolResult result = client.callTool(
                new CallToolRequest("getWeather", Map.of("city", "Paris")));

        System.out.println("Tool result: " + result.content());
    }
}

WeatherClient is a runnable class with a main method, importing McpClient/McpSyncClient and HttpClientSseClientTransport to connect to the server, and CallToolRequest/CallToolResult/LoggingLevel to invoke the tool and read its result. Inside main, the loggingConsumer lambda runs once per notification the server sends, so both the DEBUG and INFO messages from WeatherTools are printed as they arrive; calling setLoggingLevel(LoggingLevel.DEBUG) tells the server not to filter anything out, while callTool(...) triggers the actual tool execution that produces those log notifications in the first place.

2.5 Output

Running WeatherClient.main against the server above calls the getWeather tool for “Paris” and prints the following, in order:

[DEBUG] weather-tool: Received weather request for city: Paris
[INFO] weather-tool: Weather lookup completed for city: Paris
Tool result: Sunny, 25°C in Paris

The two [DEBUG]/[INFO] lines come from the loggingConsumer firing as each exchange.loggingNotification(...) call arrives from WeatherTools, and they both print before the final Tool result line — proof that the log notifications travel to the client while getWeather is still running, not after callTool(...) returns.

3. Conclusion

MCP logging gives tool servers a dedicated, level-filtered channel for reporting what’s happening during execution, without overloading the tool’s actual return value. In Spring AI, enabling it is mostly configuration — spring.ai.mcp.server.capabilities.logging=true on the server — plus a couple of exchange.loggingNotification(...) calls inside a tool and a loggingConsumer on the client. For anything beyond trivial tools, this is a cheap way to get real observability into what a remote tool is doing while it runs.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button