Logging MCP Protocol When Using stdio, Part II
Learn different ways to log the MCP JSON protocol over stdio, with an intro to MCP and a deep dive into its key components and their nuances.
Join the DZone community and get the full member experience.
Join For FreeIn Part 1, we introduced the challenge of logging MCP’s stdio communication and outlined three powerful techniques to solve it. Now, let’s get our hands dirty. This part provides a complete, practical walkthrough, demonstrating how to apply these concepts by building a Spring AI-based MCP server from scratch, configuring a GitHub Copilot client, and even creating a custom client to showcase the full power of the protocol.
Copilot Conversation Illustration
Note: I would have been glad to render the MCP logs also in the same way, but they are too verbose when formatted. Rendering without formatting won't add visual immediate value.
Prerequisites
These are not strong prerequisites.
You can use any other language, IDE, and carry out the same experiments.
That said, I am using:
- Spring Tools for Eclipse
- Java 24
- Maven
I originally began this write-up to explore various techniques for debugging the stdio logging of MCP JSON within the context of Spring AI and MCP. Along the way, I also touched on several core MCP concepts that are relevant beyond just logging. Even if teeing seems unnecessary in your current setup, understanding the concept can still prove useful in other contexts. The hope is that even if your stack looks different, the ideas and patterns discussed here will still resonate and map cleanly to your environment.
While the examples use a specific stack, these are not hard prerequisites. You’re free to use any language, SDK, IDE, or tooling that suits your setup. Similar exercises should be feasible in most modern stacks, with the underlying concepts staying consistent across environments.
MCP Server Setup
Here is what the MCP server project looks like in Eclipse:

I may not capture the complete code here, but it will cover what I presume are the necessary upfront bits here. The whole code will be shared towards the end.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath></relativePath> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mymcpserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mymcpserver</name>
<description>mymcpserver</description>
<properties>
<java.version>24</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.application.name=mymcpserver
spring.main.web-application-type=none
spring.ai.mcp.server.name=brand-z-sports-store
spring.ai.mcp.server.version=0.0.1
#STDIO will work if banner is disabled
spring.main.banner-mode=off
logging.pattern.console=
#just ensure below path is there and accessible
mine.mcp.logging.path=/tmp/logs
mine.mcp.logging.countries-file-name=countries.txt
mine.mcp.logging.combined-file-name=combined.txt
mine.mcp.logging.in-file-name=in.txt
mine.mcp.logging.out-file-name=out.txt
mine.mcp.logging.images-server-url=http://localhost:8080/images/
## Model Context Protocol Server Configuration
logging.file.name=${mine.mcp.logging.path}/spring.log
logging.level.root=ERROR
logging.level.io.modelcontextprotocol.server.transport=DEBUG
logging.level.com.eg.mcp.mytransport=DEBUG
#logging.level.com.eg.mcp=DEBUG
logging.level.com.eg.mcp.providers.tools=DEBUG
#logging.level.io.modelcontextprotocol.spec=DEBUG
Remember to use existing and reachable locations for the logging file.
MyMcpServerApplication.java
package com.eg.mcp;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import com.eg.mcp.providers.others.StoreMcpCompleteProvider;
import com.eg.mcp.providers.others.StoreMcpPromptProvider;
import com.eg.mcp.providers.others.StoreMcpResourceProvider;
import com.eg.mcp.providers.tools.StoreResourceNowToolsProvider;
import com.eg.mcp.providers.tools.StoreToolsProvider;
import com.eg.mcp.utils.McpLoggingProperties;
import com.logaritex.mcp.spring.SpringAiMcpAnnotationProvider;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
@SpringBootApplication
@EnableConfigurationProperties(McpLoggingProperties.class)
public class MyMcpServerApplication {
private static final Logger logger = LoggerFactory.getLogger(MyMcpServerApplication.class);
private FileOutputStream fos;
private FileOutputStream fis;
private FileOutputStream fcs;
private final McpLoggingProperties mcpLoggingProperties;
public MyMcpServerApplication(McpLoggingProperties mcpLoggingProperties) {
this.mcpLoggingProperties = mcpLoggingProperties;
}
@PostConstruct
private void open() throws FileNotFoundException {
//just ensure path is there and accessible
//follow best practices to avoid hardcoding
File dir = new File(mcpLoggingProperties.path());
PrintStream originalOutputStream = System.out;
InputStream originalInputStream = System.in;
fos = new FileOutputStream(new File(dir, mcpLoggingProperties.outFileName()));
fis = new FileOutputStream(new File(dir, mcpLoggingProperties.inFileName()));
fcs = new FileOutputStream(new File(dir, mcpLoggingProperties.combinedFileName()));
TeeOutputStream to = new TeeOutputStream(originalOutputStream, new TeeOutputStream(fos, fcs));
PrintStream ps = new PrintStream(to);
TeeInputStream ti = new TeeInputStream(originalInputStream, new TeeOutputStream(fis, fcs));
System.setOut(ps);
System.setIn(ti);
}
@PreDestroy
private void close() {
IOUtils.closeQuietly(fos);
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(fcs);
}
@Bean
public ToolCallbackProvider brandZTools(StoreToolsProvider storeToolsProvider, StoreResourceNowToolsProvider storeResourceNowToolsProvider) {
return MethodToolCallbackProvider.builder().toolObjects(storeToolsProvider, storeResourceNowToolsProvider).build();
}
@Bean
public List<SyncResourceSpecification> brandZPlaceHolderResources(StoreMcpResourceProvider storeMcpResourceProvider
) {
List<SyncResourceSpecification> specifications = new ArrayList<>();
createPlaceHolderFaqResource(specifications);
List<SyncResourceSpecification> otherResourceSpecifications = SpringAiMcpAnnotationProvider.createSyncResourceSpecifications(List.of(storeMcpResourceProvider));
for (SyncResourceSpecification otherResourceSpecification : otherResourceSpecifications) {
logger.debug("Adding MCP resource {}", otherResourceSpecification.resource().name());
specifications.add(otherResourceSpecification);
}
return specifications;
}
private void createPlaceHolderFaqResource(List<SyncResourceSpecification> specifications) {
McpSchema.Resource mcpResource = new McpSchema.Resource(
"mcp://brandz/store/faqs", // URI for MCP to reference
"store_faqs", // Name (for UI)
"content of store faqs in plain text", // Description (for LLM)
MediaType.TEXT_PLAIN_VALUE, // MIME type
null // Optional annotations
);
logger.debug("Adding faqs ");
var resourceSpecification = new McpServerFeatures.SyncResourceSpecification(mcpResource, (exchange, request) -> {
return new McpSchema.ReadResourceResult(List.of(
new McpSchema.TextResourceContents(
request.uri(),
MediaType.TEXT_PLAIN_VALUE,
"placeholder faq"
)
));
});
specifications.add(resourceSpecification);
}
@Bean
public List<SyncPromptSpecification> promptSpecs(StoreMcpPromptProvider storeMcpPromptProvider) {
return SpringAiMcpAnnotationProvider.createSyncPromptSpecifications(List.of(storeMcpPromptProvider));
}
@Bean
public List<SyncCompletionSpecification> completionSpecs(StoreMcpCompleteProvider storeMcpCompleteProvider) {
return SpringAiMcpAnnotationProvider.createSyncCompleteSpecifications(List.of(storeMcpCompleteProvider));
}
public static void main(String[] args) {
SpringApplication.run(MyMcpServerApplication.class, args);
}
}
Note: The main purpose here is to show how we leverage Tee[Input/Output]Stream. However, we do cover some MCP concepts, too.
StoreToolsProvider.java
package com.eg.mcp.providers.tools;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.eg.mcp.models.SportsItem;
import com.eg.mcp.models.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
@Service
public class StoreToolsProvider {
private static final Logger logger = LoggerFactory.getLogger(StoreToolsProvider.class);
private final State state;
public StoreToolsProvider(State state) {
super();
this.state = state;
}
@Tool(name = "get_store_speciality", description = "Describe whats special or unique about Brand Z sports store")
public String getStoreSpeciality() {
return "Brand Z Sports store only sells sports equipments or sporting goods that it manufactures";
}
@Tool(name = "get_items", description = "Get a list of sports equipments or sporting goods that Brand Z Sports store sells.")
public List<String> getSportsEquipments() {
List<String> labels = Arrays.stream(SportsItem.values())
.map(SportsItem::label)
.collect(Collectors.toList());
return labels;
}
@Tool(name = "get_selling_price_currency", description = "Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view.")
public String getSportsEquipmentSellingPriceCurrency() {
return state.getCurrency();
}
@Tool(name = "get_selling_price_of_item", description = "Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view")
public float getSellingPriceOfItem(String itemName) {
SportsItem sportsItem = SportsItem.labelOf(itemName);
Float price = null;
if (sportsItem != null) {
price = sportsItem.price();
}
if (price == null) {
throw new RuntimeException("Brand Z Sorts store does not stock " + itemName);
}
return price;
}
@Tool(name = "get_details_of_item", description = "Get details of items that Brand Z Sports store sells.")
public String getDetailOfItem(String itemName) {
SportsItem sportsItem = SportsItem.labelOf(itemName);
String detail = null;
if (sportsItem != null) {
detail = sportsItem.detail();
}
if (detail == null) {
throw new RuntimeException("Brand Z Sports store does not stock " + itemName);
}
return detail;
}
@Tool(name = "add_to_cart_item", description = "Add to cart item by specifying item name and its quantity")
public void addToCart(String itemName, int quantity) {
logger.debug("addingTocart " + itemName + " with quantity=" + quantity);
state.addToCart(itemName, quantity);
}
@Tool(name = "change_quantity_of_cart_item", description = "Change the quantity of the specified cart item to specified quantity")
public void changeQuantityOfCartItem(String itemName, int quantity) {
state.changeQuantityOfCartItem(itemName, quantity);
}
@Tool(name = "remove_item_from_cart_completely", description = "Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart.")
public void removeFromCart(String itemName) {
state.removeFromCart(itemName);
}
@Tool(name = "checkout_and_pay", description = "Check out items in the cart. Payment is automatic. After checkout order is available as last order.")
public void checkout() {
state.checkout();
}
}
That is our main ToolsProvider class. Also, have a look at these lines in our main Application class.
@Bean
public ToolCallbackProvider brandZTools(StoreToolsProvider storeToolsProvider, StoreResourceNowToolsProvider storeResourceNowToolsProvider) {
return MethodToolCallbackProvider.builder().toolObjects(storeToolsProvider, storeResourceNowToolsProvider).build();
}
StoreResourceNowToolsProvider is also a Tools provider. It will come to it after a while.
MyStdioServerTransportProvider.java
Note on implementation: In earlier versions (e.g., spring-ai-bom/1.0.0-M6), it was possible to achieve this logging via delegation. However, due to changes in the current SDK implementation, directly modifying the StdioServerTransportProvider or copying its relevant parts was necessary to inject the custom logging.
The complete code for this class won't be reproduced here; only the key changes will be highlighted.
/*
* Copied from StdioServerTransportProvider
* with some changes
*/
package com.eg.mcp.mytransport;
@Component
public class MyStdioServerTransportProvider implements McpServerTransportProvider {
private static final Logger logger = LoggerFactory.getLogger(MyStdioServerTransportProvider.class);
//Omitting most of the copied code here
/**
* Implementation of McpServerTransport for the stdio session.
*/
private class StdioMcpSessionTransport implements McpServerTransport {
//Omitting most of the copied code here
/**
* Starts the outbound processing thread that writes JSON-RPC messages to stdout.
* Messages are serialized to JSON and written with a newline delimiter.
*/
private void startOutboundProcessing() {
Function<Flux<JSONRPCMessage>, Flux<JSONRPCMessage>> outboundConsumer = messages -> messages // @formatter:off
.doOnSubscribe(subscription -> outboundReady.tryEmitValue(null))
.publishOn(outboundScheduler)
.handle((message, sink) -> {
if (message != null && !isClosing.get()) {
try {
String jsonMessage = objectMapper.writeValueAsString(message);
// Escape any embedded newlines in the JSON message as per spec
jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n");
logger.debug("Sending JSON message: {}", jsonMessage);
synchronized (outputStream) {
outputStream.write(jsonMessage.getBytes(StandardCharsets.UTF_8));
outputStream.write("\n".getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
sink.next(message);
}
catch (IOException e) {
if (!isClosing.get()) {
logger.error("Error writing message", e);
sink.error(new RuntimeException(e));
}
else {
logger.debug("Stream closed during shutdown", e);
}
}
}
else if (isClosing.get()) {
sink.complete();
}
})
.doOnComplete(() -> {
isClosing.set(true);
outboundScheduler.dispose();
})
.doOnError(e -> {
if (!isClosing.get()) {
logger.error("Error in outbound processing", e);
isClosing.set(true);
outboundScheduler.dispose();
}
})
.map(msg -> (JSONRPCMessage) msg);
outboundConsumer.apply(outboundSink.asFlux()).subscribe();
} // @formatter:on
}
}
Note: We wrote this class to log the outgoing MCP protocol, which was not being logged in StdioServerTransportProvider.StdioMcpSessionTransport. Unfortunately, with the way the original code was written, I was forced to copy and paste the code into a new class file and insert the log statement:
logger.debug("Sending JSON message: {}", jsonMessage);
MyStdioServerTransportProvider is decorated by @Component, which causes it to become a bean. This bean is used instead of io.modelcontextprotocol.server.transport.StdioServerTransportProvider.
This works because in org.springframework.ai.mcp.server.autoconfigure.McpServerAutoConfiguration, we have this:
@Bean
@ConditionalOnMissingBean
public McpServerTransportProvider stdioServerTransport() {
return new StdioServerTransportProvider();
}
What this means is that if we don't provide our own McpServerTransportProvider, StdioServerTransportProvider will be used instead. Otherwise, our provided bean will be used. So we did not do anything illegal here. This bean was meant to be replaced if needed. The original implementation, as coded, offered us limited opportunities to extend/delegate, or insert the extra logging we needed.
Even if this is not attempted, we do have the other Tee-based approach.
Note:
- The use of
@Tool, which is used forToolfunctions. That said, there is also a community project that brings in various other annotations, including@McpResourceand others, which we will also briefly have a look at. - It's important to give sensible descriptions and names in the
Toolannotation. They must be meaningful to the LLM for it to make sense of the tools and use them correctly.
MCP Server Build
Let's build the project using the following at the top level of the multi-module Maven project:
mvn clean package
That should deliver us a jar file — mymcpserver/target/mymcpserver-0.0.1-SNAPSHOT.jar.
For convenience, let’s manually copy this JAR to a separate, more stable location — for example, d:/my-mcp-server-dist/mymcpserver-0.0.1-SNAPSHOT.jar.
Why do this?
Because when the MCP server is running and a rebuild is triggered during development, the JAR file in the target might be locked or in use, causing build failures. Moving it to a distribution folder ensures your MCP client can consistently launch it without interfering with your active codebase.
Note that the mymcpserver module includes a sister project, image-server, a simple Spring Boot app that serves images. This image-server project is optional. You can launch it by running the following command from within the image-server directory:
java -jar target/image-server-0.0.1-SNAPSHOT.jar
It simply serves images online at localhost:8080 for use by our MCP server and during the Copilot/MCP client demonstration.
Github Copilot Eclipse Plugin
This is the marketplace page for the GitHub Copilot Eclipse plugin. I prefer installing it from within Eclipse.

Here, search for "GitHub Copilot" and press the "Go" Button.

Press the install button and carry out the remaining steps. You might be prompted to restart Eclipse. Version 0.8.0 was the version available at the time of writing this. 0.10.0 is available now and behaves mostly the same for the purpose of this article.
Github Copilot Eclipse Plugin Configuration
Visit Copilot > Edit Preferences.

Then reach here:

You can enable GitHub Copilot enterprise access using the GitHub Enterprise Authentication Endpoint textbox shown here. That's just one of the many ways.
GitHub Copilot can still be used with free access, with certain limitations.
Further sub-navigate to GitHub Copilot > MCP.

Let's change the configuration to:
{
"servers": {
"brand-z-sports-store": {
"type": "stdio",
"command": "D:/dev/pub/java/jdk-24.0.2/bin/java",
"args": [
"-jar",
"D:/my-mcp-server-dist/mymcpserver-0.0.1-SNAPSHOT.jar"
],
"env": {}
}
}
}

Press the "Apply and close" button.
Revisit Copilot > Edit Preferences > GitHub CoPilot > MCP.

As I went about coding, the number of tools actually increased from what you can see listed here.
Visit Copilot > Open Chat.

Ensure Agent Mode Is Enabled
This is the much-discussed Agent Mode, where GitHub Copilot can go beyond suggestions — it can actively write, modify, and refactor your code, among other capabilities. In version 0.10.0, Agent mode is no longer just in Preview mode. Do please enable Agent Mode.
Checking the Logs
- Shut down Eclipse completely.
- Optionally, if you like, for clarity, delete the various existing log files (e.g., in.txt, out.txt, combined.txt, spring.log).
- Restart Eclipse, wait for Copilot to connect to the MCP server.
- Observe how the log files update in real time as the connection is established.
You should now see MCP protocol communication captured clearly in JSON, both from the logger and your Tee-based file output.
Let's have a quick look at the various logs. I've deliberately not formatted them. It would occupy precious visual space then. Feel free to examine the MCP protocol in more detail. Just capturing what's available on the startup of CoPilot.
in.txt
{"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"brand-z-sports-store","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
{"method":"notifications/initialized","jsonrpc":"2.0"}
{"method":"tools/list","jsonrpc":"2.0","id":1}
out.txt
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"brand-z-sports-store","version":"0.0.1"}}}
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_tennis_net_image","description":"Returns the URL of the image of the tennis net","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_football_image","description":"Returns the URL of the image of the football","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"checkout_and_pay","description":"Check out items in the cart. Payment is automatic. After checkout order is available as last order.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"change_quantity_of_cart_item","description":"Change the quantity of the specified cart item to specified quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}},{"name":"get_cart_content_in_markdown","description":"get cart content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_last_order_content_in_xml","description":"get last order content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_currency","description":"Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"remove_item_from_cart_completely","description":"Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_markdown","description":"get last order content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_details_of_item","description":"Get details of items that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_cart_content_in_xml","description":"get cart content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_raquet_image","description":"Returns the URL of the image of the tennis raquet","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_store_speciality","description":"Describe whats special or unique about Brand Z sports store","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_ball_image","description":"Returns the URL of the image of the tennis ball","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_cart_content_in_json","description":"get cart content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_items","description":"Get a list of sports equipments or sporting goods that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_of_item","description":"Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_json","description":"get last order content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"add_to_cart_item","description":"Add to cart item by specifying item name and its quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}}]}}
combined.txt
{"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"brand-z-sports-store","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"brand-z-sports-store","version":"0.0.1"}}}
{"method":"notifications/initialized","jsonrpc":"2.0"}
{"method":"tools/list","jsonrpc":"2.0","id":1}
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_tennis_net_image","description":"Returns the URL of the image of the tennis net","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_football_image","description":"Returns the URL of the image of the football","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"checkout_and_pay","description":"Check out items in the cart. Payment is automatic. After checkout order is available as last order.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"change_quantity_of_cart_item","description":"Change the quantity of the specified cart item to specified quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}},{"name":"get_cart_content_in_markdown","description":"get cart content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_last_order_content_in_xml","description":"get last order content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_currency","description":"Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"remove_item_from_cart_completely","description":"Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_markdown","description":"get last order content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_details_of_item","description":"Get details of items that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_cart_content_in_xml","description":"get cart content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_raquet_image","description":"Returns the URL of the image of the tennis raquet","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_store_speciality","description":"Describe whats special or unique about Brand Z sports store","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_ball_image","description":"Returns the URL of the image of the tennis ball","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_cart_content_in_json","description":"get cart content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_items","description":"Get a list of sports equipments or sporting goods that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_of_item","description":"Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_json","description":"get last order content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"add_to_cart_item","description":"Add to cart item by specifying item name and its quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}}]}}
spring.log
2025-08-14T21:45:29.777+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"brand-z-sports-store","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
2025-08-14T21:45:29.909+05:30 DEBUG 12608 --- [mymcpserver] [pool-4-thread-1] c.e.m.m.MyStdioServerTransportProvider : Sending JSON message: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"brand-z-sports-store","version":"0.0.1"}}}
2025-08-14T21:45:29.923+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"notifications/initialized","jsonrpc":"2.0"}
2025-08-14T21:45:29.940+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"tools/list","jsonrpc":"2.0","id":1}
2025-08-14T21:45:29.963+05:30 DEBUG 12608 --- [mymcpserver] [pool-4-thread-1] c.e.m.m.MyStdioServerTransportProvider : Sending JSON message: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_tennis_net_image","description":"Returns the URL of the image of the tennis net","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_football_image","description":"Returns the URL of the image of the football","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"checkout_and_pay","description":"Check out items in the cart. Payment is automatic. After checkout order is available as last order.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"change_quantity_of_cart_item","description":"Change the quantity of the specified cart item to specified quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}},{"name":"get_cart_content_in_markdown","description":"get cart content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_last_order_content_in_xml","description":"get last order content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_currency","description":"Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"remove_item_from_cart_completely","description":"Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_markdown","description":"get last order content formatted in markdown","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_details_of_item","description":"Get details of items that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_cart_content_in_xml","description":"get cart content formatted in xml","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_raquet_image","description":"Returns the URL of the image of the tennis raquet","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_store_speciality","description":"Describe whats special or unique about Brand Z sports store","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_tennis_ball_image","description":"Returns the URL of the image of the tennis ball","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_cart_content_in_json","description":"get cart content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_items","description":"Get a list of sports equipments or sporting goods that Brand Z Sports store sells.","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"get_selling_price_of_item","description":"Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"}},"required":["itemName"],"additionalProperties":false}},{"name":"get_last_order_content_in_json","description":"get last order content formatted in json","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"add_to_cart_item","description":"Add to cart item by specifying item name and its quantity","inputSchema":{"type":"object","properties":{"itemName":{"type":"string"},"quantity":{"type":"integer","format":"int32"}},"required":["itemName","quantity"],"additionalProperties":false}}]}}
Since we’ve been knee-deep in logs, it’s worth also glancing through the MCP Server Logging Support document. This section explains how the MCP server supports structured logging, enabling log messages to be sent to clients with severity levels. But that's a separate topic — and we’re not diving into it here.
Chatting With CoPilot
I'm not going to walk you through how Agent Mode enables GitHub Copilot to generate and modify code — all while keeping you in control of the final decisions. If you haven’t explored it yet, I trust you’ll discover it soon enough.
But I can’t resist sharing a few conversations that illustrate how MCP-powered interactions feel strikingly more agentic, intelligent, contextual, conversational, and tool-aware, as shown in the following examples.
How Workspace Context Changes Agent Behavior
Showing below is the conversation in two flavours of my Copilot. In one, I have a totally empty Eclipse workspace. In another, I have the server code in the workspace. I am, in general, trying to ask the same questions. I also have the Java class StoreToolsProvider.java open in the Eclipse editor. It's also automatically added to the Copilot context.
In both, the MCP configuration is done in the same way. I have been running them separately, not together. The actual text might vary a little on each run, but you will be able to spot the main differences.
| Empty workspace: workspace without MCP server code | Workspace with MCP server code |
|---|---|
|
|
|
| Empty workspace: workspace without MCP server code | Workspace with MCP server code |
|---|---|
| Username | Username |
| Hi | Hi |
| GitHub Copilot | GitHub Copilot |
| Hello! How can I assist you with your project today? | Hello! How can I assist you with your project today? |
| Username | Username |
| What items are available under Brand Z Sports Store? | What items are available under Brand Z Sports Store? |
| GitHub Copilot | GitHub Copilot |
| Confirm MCP Tool: get_items - brand-z-sports-store(MCP Server) | |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_items" to run? Continue/Cancel | |
| ✅ Running MCP tool: get_items - brand-z-sports-store (MCP Server) | |
| The items available under Brand Z Sports Store are: Tennis ball, Tennis racquet, Football, and Tennis net. Let me know if you need more help! | The items available under Brand Z Sports Store are Tennis ball, Tennis raquet, Football, and Tennis net. Let me know if you need details or prices for any of these items. |
| Username | Username |
| Do you have soccer balls? | Do you have soccer balls? |
| GitHub Copilot | GitHub Copilot |
| Brand Z Sports Store offers footballs, but "soccer ball" is not explicitly listed. In many regions, "football" refers to a soccer ball. Would you like details or confirmation about the football item to ensure it matches your needs? | Yes, Brand Z Sports Store does have soccer balls. In the system, they are listed as "Football" and described as "Also known as a soccer ball. This is not a rugby ball." |
| Username | Username |
| more details please | |
| GitHub Copilot | GitHub Copilot |
| Confirm MCP Tool: get_details_of_item - brand-z-sports-store(MCP Server) | |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_details_of_item" to run? Continue/Cancel | |
| ✅ Running MCP tool: get_details_of_item - brand-z-sports-store (MCP Server) | |
| The football available at Brand Z Sports Store is also known as a soccer ball. It is specifically a soccer ball and not a rugby ball. If you need more information or want details about other items, let me know! | |
| Username | Username |
| Do you have futball | Do you have futball |
| GitHub Copilot | GitHub Copilot |
| Brand Z Sports Store offers footballs, which are also known as soccer balls or futballs in some regions. So yes, futballs are available at Brand Z Sports Store. If you need more details or pricing, let me know! | Brand Z Sports Store lists "Football" as one of its items, and in the details, it is described as "Also known as a soccer ball. This is not a rugby ball." If you are referring to "futball" as soccer, then yes, the store does have it. |
I could keep having conversations and showing more differences. The differences stem from Copilot using the additional context to its advantage, particularly in cases where the source code was available. Where the additional source code had no impact, depending on the question, the replies were similar.
We have explored Tool functions. They are capable of being invoked, and behind the scenes, they can do anything. Therefore, it's always good that the system asks the user for permission before invoking the tools.
We will now review the resources and focus on the workspace, which does not contain the MCP server code.
Serving Resources
After trying to serve resources for which I tried two approaches, one including @MCPResource from https://github.com/spring-ai-community/mcp-annotations, I realized that this won't work with CoPilot.
This realization came after looking at the logs.
For example, if we look at server.log:
2025-08-14T21:45:29.777+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"brand-z-sports-store","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
2025-08-14T21:45:29.909+05:30 DEBUG 12608 --- [mymcpserver] [pool-4-thread-1] c.e.m.m.MyStdioServerTransportProvider : Sending JSON message: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"brand-z-sports-store","version":"0.0.1"}}}
2025-08-14T21:45:29.923+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"notifications/initialized","jsonrpc":"2.0"}
2025-08-14T21:45:29.940+05:30 DEBUG 12608 --- [mymcpserver] [pool-2-thread-1] c.e.m.m.MyStdioServerTransportProvider : Received JSON message: {"method":"tools/list","jsonrpc":"2.0","id":1}
We can note that the server has informed the client about its capabilities here:
Sending JSON message: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"brand-z-sports-store","version":"0.0.1"}}}
Subsequently, the server is asked about the tools.
Received JSON message: {"method":"tools/list","jsonrpc":"2.0","id":1}
Please read more about the resources/list here.
But there is no similar enquiry about resources. Since there is no enquiry about resources, the server cannot inform about resources. I rewrote all my resource code using @Tool for now.
This brings us to the class we had mentioned earlier, namely StoreResourceNowToolsProvider.java.
StoreResourceNowToolsProvider.java
package com.eg.mcp.providers.tools;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import com.eg.mcp.models.Cart;
import com.eg.mcp.models.Order;
import com.eg.mcp.models.SportsItem;
import com.eg.mcp.models.State;
import com.eg.mcp.utils.MarkdownMapper;
import com.eg.mcp.utils.McpLoggingProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
/*
* Initially was planning of making this a class for Resources and not Tools.
* But later made it a Tools class.
*/
@Service
public class StoreResourceNowToolsProvider {
private static final Logger logger = LoggerFactory.getLogger(StoreResourceNowToolsProvider.class);
private static final String RETURNS = "Returns the URL of the image of the ";
private final ObjectMapper jsonMapper;
private final XmlMapper xmlMapper;
private final MarkdownMapper markdownMapper;
private final State state;
private final McpLoggingProperties mcpLoggingProperties;
public StoreResourceNowToolsProvider(ObjectMapper jsonMapper, State state, McpLoggingProperties mcpLoggingProperties) {
super();
this.jsonMapper = jsonMapper;
this.mcpLoggingProperties = mcpLoggingProperties;
this.state = state;
this.xmlMapper = XmlMapper.builder().build();
this.markdownMapper = new MarkdownMapper();
}
@Tool(name = "get_tennis_ball_image", description = RETURNS + "tennis ball")
public String getTennisBallImage() {
return mduri(SportsItem.TENNIS_BALL);
}
@Tool(name = "get_tennis_net_image", description = RETURNS + "tennis net")
public String getTennisNetImage() {
return mduri(SportsItem.TENNIS_NET);
}
@Tool(name = "get_tennis_raquet_image", description = RETURNS + "tennis raquet")
public String getTennisRaquetImage() {
return mduri(SportsItem.TENNIS_RAQUET);
}
@Tool(name = "get_football_image", description = RETURNS + "football")
public String getFootballImage() {
return mduri(SportsItem.FOOTBALL);
}
@Tool(name = "get_cart_content_in_json", description = "get cart content formatted in json")
public String cartjson() throws IOException {
Cart cart = state.toCart();
return jsonMapper.writeValueAsString(cart);
}
@Tool(name = "get_cart_content_in_xml", description = "get cart content formatted in xml")
public String cartxml() throws IOException {
Cart cart = state.toCart();
return xmlMapper.writeValueAsString(cart);
}
@Tool(name = "get_cart_content_in_markdown", description = "get cart content formatted in markdown")
public String cartmd() {
Cart cart = state.toCart();
return markdownMapper.writeValueAsString(cart);
}
@Tool(name = "get_last_order_content_in_json", description = "get last order content formatted in json")
public String lastorderjson() throws IOException {
Order lastOrder = state.getLastOrder();
return jsonMapper.writeValueAsString(lastOrder);
}
@Tool(name = "get_last_order_content_in_xml", description = "get last order content formatted in xml")
public String lastorderxml() throws IOException {
Order lastOrder = state.getLastOrder();
return xmlMapper.writeValueAsString(lastOrder);
}
@Tool(name = "get_last_order_content_in_markdown", description = "get last order content formatted in markdown")
public String lastordermd() {
logger.debug("Entered lastordermd");
Order lastOrder = state.getLastOrder();
logger.debug("Got lastorder");
String content = markdownMapper.writeValueAsString(lastOrder);
logger.debug("Got content");
logger.debug("Exiting lastordermd");
return content;
}
private String mduri(SportsItem sportsItem) {
return mcpLoggingProperties.imagesServerUrl() + sportsItem.touri();
}
}
In this connection, see: https://modelcontextprotocol.io/docs/learn/server-concepts#resources-context-data.
I reproduced the relevant section here for quick reading.
Resources expose data from files, APIs, databases, or any other source that an AI needs to understand context. Applications can access this information directly and decide how to use it - whether that’s selecting relevant portions, searching with embeddings, or passing it all to the model.
Resources are application-driven, giving hosts flexibility in how they retrieve, process, and present available context. Common interaction patterns include tree or list views for browsing resources in familiar folder-like structures, search and filter interfaces for finding specific resources, automatic context inclusion based on heuristics or AI selection, and manual selection interfaces.
Sometimes, it helps to see slightly older explanations. The relevant section here is for quick reading.
Resources are designed to be application-controlled, meaning that the client application can decide how and when they should be used. Different MCP clients may handle resources differently. For example:
- Claude Desktop currently requires users to explicitly select resources before they can be used
- Other clients might automatically select resources based on heuristics
- Some implementations may even allow the AI model itself to determine which resources to use
Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a model-controlled primitive such as Tools.
Bottom line, if I were to write a resource code and need it to be discoverable by Copilot, just like how tools are, I would have to decorate the method with both @McpResource and @Tool, or write a bridge tool. You can choose either approach. In my scenario, since I wanted these to be usable by the LLM directly, I chose a plain @Tool route and did not bother about coding them as a resource, even though functionally I was using them as resources.
Tools Improvement Area
One area I encourage readers to improve upon is making their tools more generic. In a real-world scenario, having specific methods like getTennisRacquetImage() or getFootballImage() quickly becomes unmaintainable. Just as you would with resource templates, aim to create more flexible tool methods that accept parameters, allowing the LLM to invoke them dynamically based on intent. Be sure to provide clear and detailed descriptions, so the LLM can understand how and when to use the tool effectively.
MCP Resources, Prompts, and Completions
Back to resources, I have not thrown away MCP resources, prompts, and completions. Please see the MyMcpServerApplication.
- Method
brandZPlaceHolderResources()for my resource implementations, leveraging bothMcpServerFeatures.SyncResourceSpecificationandSpringAiMcpAnnotationProvider.createSyncResourceSpecifications()with@McpResource. - Method
promptSpecs()for my prompts implementation. - Method
completionSpecs()for my completions implementation.
We will revisit these and discuss them in the MCP client section later.
Spring AI-Powered Agentic LLM-Based Workflow Orchestration
| Empty workspace: workspace without mcp server code |
|---|
| Username |
| Hi |
| GitHub Copilot |
| Hello! How can I assist you with your project today? |
| Username |
| What are the items available under Brand Z Sports Store? |
| GitHub Copilot |
| The items available under Brand Z Sports Store are: Tennis ball, Tennis racquet, Football, and Tennis net. Let me know if you need more help! |
| Username |
| Show me in html details of the tennis ball and its picture |
| GitHub Copilot |
| Confirm MCP Tool: get_details_of_item - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_details_of_item" to run? Continue/Cancel |
| Confirm MCP Tool: get_tennis_ball_image - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_tennis_ball_image" to run? Continue/Cancel |
| Here are the details of a tennis ball from Brand Z Sports Store, formatted in HTML: |
<div>
<h2>Tennis Ball</h2>
<p>Type: Standard Tennis ball</p>
<img src="http://localhost:8080/images/tennis_ball.png" alt="Tennis Ball" style="width:200px;">
</div>
Did you notice that when we enquired about the items this time, it did not run the "get_items" tool?
| Empty workspace: workspace without MCP server code |
|---|
| This displays the tennis ball's type and its image. Let me know if you need more details or a different format! |
| Username |
| show me same in markdown |
| GitHub Copilot |
| Here is the tennis ball information in Markdown format: |
## Tennis Ball
- Type: Standard Tennis ball

Before proceeding with the chat, manually create a simple empty Eclipse project named "files" in the workspace.
| empty workspace: workspace without MCP server code |
|---|
| You can use this Markdown to display the tennis ball details and its image. Let me know if you need it in a specific file! |
| Username |
| I have created an empty project named "files" in the previously empty Eclipse workspace. can you create a file named abc.MD in the files project. This file abc.MD should contain in it details of the Tennis ball and a picture of same. This file abc.MD should be formatted in markdown format. |
| GitHub Copilot |
| ✅ Running create_file tool |
| The file abc.MD has been created in your files project. It contains the details of the Tennis ball and its picture, formatted in markdown. You can open this file in your editor to view or further edit the content. If you need any more changes or want to add details for other items, let me know! |
![]() |
|
Now we must verify that we wish to retain these changes.


Now, close the file before proceeding so that it's no longer open in the Eclipse Editor.
Note: Here we have demonstrated an agentic LLM-based workflow orchestration.
While the LLM provides intelligent orchestration, a custom client could further augment or even replace the LLM's orchestration decisions with programmatic control, offering greater precision or deterministic behavior when required.
| workspace without MCP server code |
|---|
| Username |
| What's the cost of a Tennis net? |
| GitHub Copilot |
| Confirm MCP Tool: get_selling_price_of_item - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_selling_price_of_item" to run? Continue/Cancel |
| ✅ Running MCP tool: get_selling_price_of_item - brand-z-sports-store(MCP Server) |
| The cost of a Tennis net at Brand Z Sports Store is 10.0 (currency not specified). If you need the currency or more details, let me know! |
| Username |
| yes please whats the currency? |
| GitHub Copilot |
| Confirm MCP Tool: get_selling_price_currency - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_selling_price_currency" to run? Continue/Cancel |
| ✅ Running MCP tool: get_selling_price_currency - brand-z-sports-store(MCP Server) |
| The cost of a Tennis net at Brand Z Sports Store is 10.0 USD. If you need prices or details for other items, just let me know! |
| Username |
| Can I buy a Tennis net, two Tennis raquets, 4 balls please? |
| GitHub Copilot |
| Confirm MCP Tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/add_toCart_item" to run? Continue/Cancel |
| ✅ Running MCP tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Confirm MCP Tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/add_toCart_item" to run? Continue/Cancel |
| ✅ Running MCP tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Confirm MCP Tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/add_toCart_item" to run? Continue/Cancel |
| ✅ Running MCP tool: add_toCart_item - brand-z-sports-store(MCP Server) |
| Your order has been added to the cart: 1 Tennis net, 2 Tennis raquets, and 4 Tennis balls. Would you like to proceed to checkout or need more information about your cart? |
| Username |
| Show me my cart formatted in markdown format |
| GitHub Copilot |
| Confirm MCP Tool: get_cart_content_in_markdown - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_cart_content_in_markdown" to run? Continue/Cancel |
| ✅ Running MCP tool: get_cart_content_in_markdown - brand-z-sports-store(MCP Server) |
Cart
- **orderItems**:
- **itemName**: Tennis raquet
- **qty**: 2
- **rate**: 10.2
- **cost**: 20.4
- 
- **currency**: USD
- **itemName**: Tennis net
- **qty**: 1
- **rate**: 10.0
- **cost**: 10.0
- 
- **currency**: USD
- **itemName**: Tennis ball
- **qty**: 4
- **rate**: 10.3
- **cost**: 41.2
- 
- **currency**: USD
- **total**: 71.6 USD
Let me know if you want to proceed to checkout or need to modify your cart.
| empty workspace continued |
|---|
| Username |
| Can you download this to the "files" project in eclipse workspace and save it as cart.MD? |
| GitHub Copilot |
| ✅ Running create_file tool |
| The cart details have been saved as cart.MD in the files project of your Eclipse workspace. You can now view or edit this file directly within your project. If you need any further actions or modifications, let me know |
![]() |
|
Now we must verify that we wish to retain these changes.


Now, close the file before proceeding so that it's no longer open in the Eclipse editors.
| workspace without MCP server code |
|---|
| Username |
| Can we checkout the cart? |
| GitHub Copilot |
| Confirm MCP Tool: checkout_and_pay - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/checkout_and_pay" to run? Continue/Cancel |
| ✅ Running MCP tool: checkout_and_pay - brand-z-sports-store(MCP Server) |
| Your cart has been successfully checked out and the payment is complete. If you need details about your order or want to view your order summary, let me know! |
| Username |
| show me the order summary please |
| GitHub Copilot |
| Confirm MCP Tool: get_last_order_content_in_markdown - brand-z-sports-store(MCP Server) |
| Do you want to allow the external MCP tool "brand-z-sports-store/get_last_order_content_in_markdown" to run? Continue/Cancel |
| ✅ Running MCP tool: get_last_order_content_in_markdown - brand-z-sports-store(MCP Server) |
Here is your order summary in markdown format:
Order
• orderNumber: ORD-1753209764155
• orderDateTime: 2025-07-23T00:12:44.156735700
• orderItems:
• itemName: Tennis raquet
• qty: 2
• rate: 10.2
• cost: 20.4
• currency: USD
• itemName: Tennis net
• qty: 1
• rate: 10.0
• cost: 10.0
• currency: USD
• itemName: Tennis ball
• qty: 4
• rate: 10.3
• cost: 41.2
• currency: USD
• total: 71.6 USD
If you want this saved to a file in your workspace, let me know the filename and location!
| empty workspace continued |
|---|
| Username |
| Can you download this to the "files" project in eclipse workspace and save it as order.MD? |
| GitHub Copilot |
| Confirm MCP Tool: get_last_order_content_in_markdown - brand-z-sports-store(MCP Server) |
| ✅ Running MCP tool: get_last_order_content_in_markdown - brand-z-sports-store(MCP Server) |
| ✅ Running create_file tool |
| The order summary has been downloaded and saved as order.MD in the "files" project of your Eclipse workspace. You can now open order.MD to view your order details. If you need further assistance, let me know! |
![]() |
![]() |
Now, we must verify that we wish to retain these changes.


Now, close the file before proceeding.
You might have noticed how the LLM used the same data and presented it a bit better than what we saw when we downloaded it, especially in how it handled the currency.
Upon comparing the content presented by Copilot with the raw data we downloaded, we observed that the LLM's output was generally more intelligent and refined. A notable aspect was its decision not to display images, which is likely a security defense mechanism.
Note: Usually, LLMs enhance a tool's raw output by adding conversational elements or additional formatting, as you might observe in general LLM behavior. However, there are scenarios where this enhancement might not be desirable. If so, to prevent the LLM from adding further formatting or conversational wrappers to your tool's raw output, use the returnDirect attribute in @Tool.
MCP Custom Client
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
<relativePath></relativePath> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mymcpclient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring AI - Model Context Protocol - Client Sample</name>
<description>Simple AI Application using MCP client sample</description>
<properties>
<java.version>24</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
mcp-servers-config.json
{
"mcpServers": {
"brand-r-sports-store": {
"command": "D:/dev/pub/java/jdk-24.0.2/bin/java",
"args": [
"-jar",
"D:/my-mcp-server-dist/mymcpserver-0.0.1-SNAPSHOT.jar"
],
"env": {
}
}
}
}
application.properties
spring.application.name=mcp
spring.main.web-application-type=none
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
MyMcpClientApplication.java
package com.eg.mcp;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class MyMcpClientApplication {
public static void main(String[] args) {
SpringApplication.run(MyMcpClientApplication.class, args);
}
@Bean
public CommandLineRunner demo(List<McpSyncClient> mcpSyncClients, ConfigurableApplicationContext context) {
return args -> {
if (mcpSyncClients.size() == 1) {
McpSyncClient client = mcpSyncClients.getFirst();
ListToolsResult toolsList = client.listTools();
System.out.println("Available Tools = " + toolsList);
toolsList.tools().forEach(tool -> {
System.out.println("Tool: " + tool.name() + ", description: " + tool.description() + ", schema: "
+ tool.inputSchema());
});
ListResourcesResult listResources = client.listResources();
listResources.resources().forEach(resource -> {
System.out.println("Resource: " + resource.name() + ", " + resource.uri() + ", " + resource.description());
});
ReadResourceResult resource = client.readResource(new ReadResourceRequest("mcp://brandz/store/rules"));
System.out.println("result = " + resource);
ListPromptsResult listPrompts = client.listPrompts();
listPrompts.prompts().forEach(prompt -> {
System.out.println("Prompt: " + prompt.name() + ", " + prompt.description());
});
GetPromptResult prompt = client.getPrompt(
new GetPromptRequest("brandz-greeting", Map.of("name", "Doe")));
System.out.println("result = " + prompt);
// Completions
CompleteResult completion = client.completeCompletion(new CompleteRequest(new PromptReference("country-status"),
new CompleteRequest.CompleteArgument("countryName", "a")));
System.out.println("Completion = " + completion);
GetPromptResult geneteratedPrompt = client.getPrompt(new GetPromptRequest("generate_greeting_prompt", Map.of("name", "Doe", "style","friendly")));
System.out.println("generatedPrompt = " + geneteratedPrompt);
context.close();
}
};
}
}
It’s important to remember that for an LLM to recognize and utilize resources, prompts, or completions, they typically need to be explicitly configured as such. See the earlier discussion or refer directly to the official documentation on resources for more details.
That said, resources, prompts, and completions are primarily intended for use by the MCP client or end user. In the code example shown here, we demonstrate how an MCP client can handle these features independently, without involving an LLM. LLM-integrated usage scenarios have already been covered in previous sections.
There is an additional nuance worth bringing out.
The existing examples of resources, prompts, and completions are based on the standard samples provided in the Spring AI MCP Annotations Server. In particular, the PromptProvider.java class mostly returns hardcoded results for given inputs. This approach doesn't allow for any dynamic generation involving an agent or LLM.
That is, unless the prompt examples included an example like below:
@McpPrompt(name = "generate_greeting_prompt", description = "Generate a greeting prompt")
public PromptMessage generateGreetingPrompt(
@McpArg(name = "name", description = "The name of the person to greet") String name,
@McpArg(name = "style", description = "The style of the greeting: formal, casual, or friendly") String style) {
String prompt;
switch (style != null ? style : "friendly") {
case "formal":
prompt = "Please write a formal, professional greeting";
break;
case "casual":
prompt = "Please write a casual, relaxed greeting";
break;
case "friendly":
default:
prompt = "Please write a warm, friendly greeting";
break;
}
return new PromptMessage(Role.USER, new TextContent(prompt + " for someone named " + name + "."));
}
This example, inspired by the Python SDK prompt sample and implemented in mymcpserver/src/main/java/com/eg/mcp/providers/others/StoreMcpPromptProvider.java, doesn't return a direct result. Instead, it returns an actual `PromptMessage` that can be sent to the chat text box.
In an LLM-enabled MCP client — such as GitHub Copilot (were it to allow such integrations) — these prompts, and related completions, if any, could, with user approval, be surfaced to the LLM during conversations, allowing the model to utilize them meaningfully.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
[32m :: Spring Boot :: [39m [2m (v3.4.5)[0;39m
[2m2025-07-16T23:00:38.439+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [ main] [0;39m[36mcom.eg.mcp.MyMcpClientApplication [0;39m [2m:[0;39m Starting MyMcpClientApplication using Java 22.0.2 with PID 26828 (D:\dev\priv\wss\basics\myai\myaiws1\myai\mymcp\mymcplient\target\classes started by AL1088 in D:\dev\priv\wss\basics\myai\myaiws1\myai\mymcp\mymcplient)
[2m2025-07-16T23:00:38.441+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [ main] [0;39m[36mcom.eg.mcp.MyMcpClientApplication [0;39m [2m:[0;39m The following 1 profile is active: "chatgpt"
[2m2025-07-16T23:00:39.194+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [oundedElastic-1] [0;39m[36mi.m.c.transport.StdioClientTransport [0;39m [2m:[0;39m MCP server starting.
[2m2025-07-16T23:00:39.215+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [oundedElastic-1] [0;39m[36mi.m.c.transport.StdioClientTransport [0;39m [2m:[0;39m MCP server started
[2m2025-07-16T23:00:42.063+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [pool-2-thread-1] [0;39m[36mi.m.client.LifecycleInitializer [0;39m [2m:[0;39m Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[completions=CompletionCapabilities[], experimental=null, logging=LoggingCapabilities[], prompts=PromptCapabilities[listChanged=true], resources=ResourceCapabilities[subscribe=false, listChanged=true], tools=ToolCapabilities[listChanged=true]], Info: Implementation[name=brand-z-sports-store, title=null, version=0.0.1] and Instructions null
[2m2025-07-16T23:00:42.184+05:30[0;39m [32m INFO[0;39m [35m26828[0;39m [2m--- [mcp] [ main] [0;39m[36mcom.eg.mcp.MyMcpClientApplication [0;39m [2m:[0;39m Started MyMcpClientApplication in 4.115 seconds (process running for 4.67)
Available Tools = ListToolsResult[tools=[Tool[name=get_tennis_net_image, title=null, description=Returns the URL of the image of the tennis net, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_football_image, title=null, description=Returns the URL of the image of the football, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=checkout_and_pay, title=null, description=Check out items in the cart. Payment is automatic. After checkout order is available as last order., inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=change_quantity_of_cart_item, title=null, description=Change the quantity of the specified cart item to specified quantity, inputSchema=JsonSchema[type=object, properties={itemName={type=string}, quantity={type=integer, format=int32}}, required=[itemName, quantity], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_cart_content_in_markdown, title=null, description=get cart content formatted in markdown, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_last_order_content_in_xml, title=null, description=get last order content formatted in xml, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_selling_price_currency, title=null, description=Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view., inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=remove_item_from_cart_completely, title=null, description=Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart., inputSchema=JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_last_order_content_in_markdown, title=null, description=get last order content formatted in markdown, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_details_of_item, title=null, description=Get details of items that Brand Z Sports store sells., inputSchema=JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_cart_content_in_xml, title=null, description=get cart content formatted in xml, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_tennis_raquet_image, title=null, description=Returns the URL of the image of the tennis raquet, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_store_speciality, title=null, description=Describe whats special or unique about Brand Z sports store, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_tennis_ball_image, title=null, description=Returns the URL of the image of the tennis ball, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_cart_content_in_json, title=null, description=get cart content formatted in json, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_items, title=null, description=Get a list of sports equipments or sporting goods that Brand Z Sports store sells., inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_selling_price_of_item, title=null, description=Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view, inputSchema=JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=get_last_order_content_in_json, title=null, description=get last order content formatted in json, inputSchema=JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null], Tool[name=add_to_cart_item, title=null, description=Add to cart item by specifying item name and its quantity, inputSchema=JsonSchema[type=object, properties={itemName={type=string}, quantity={type=integer, format=int32}}, required=[itemName, quantity], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null]], nextCursor=null]
Tool: get_tennis_net_image, description: Returns the URL of the image of the tennis net, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_football_image, description: Returns the URL of the image of the football, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: checkout_and_pay, description: Check out items in the cart. Payment is automatic. After checkout order is available as last order., schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: change_quantity_of_cart_item, description: Change the quantity of the specified cart item to specified quantity, schema: JsonSchema[type=object, properties={itemName={type=string}, quantity={type=integer, format=int32}}, required=[itemName, quantity], additionalProperties=false, defs=null, definitions=null]
Tool: get_cart_content_in_markdown, description: get cart content formatted in markdown, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_last_order_content_in_xml, description: get last order content formatted in xml, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_selling_price_currency, description: Get the currency for the various items selling price. Brand Z Sports store's selling price for an item would also be the same as the cost price of the item from a vendor's point of view., schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: remove_item_from_cart_completely, description: Remove item from cart item by specifying item name. Can think that its quantity was reduced to 0. Item will no longer occur in the cart., schema: JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null]
Tool: get_last_order_content_in_markdown, description: get last order content formatted in markdown, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_details_of_item, description: Get details of items that Brand Z Sports store sells., schema: JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null]
Tool: get_cart_content_in_xml, description: get cart content formatted in xml, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_tennis_raquet_image, description: Returns the URL of the image of the tennis raquet, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_store_speciality, description: Describe whats special or unique about Brand Z sports store, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_tennis_ball_image, description: Returns the URL of the image of the tennis ball, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_cart_content_in_json, description: get cart content formatted in json, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_items, description: Get a list of sports equipments or sporting goods that Brand Z Sports store sells., schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: get_selling_price_of_item, description: Get selling price of items that Brand Z Sports store sells. Is also the item's cost from buyers point of view, schema: JsonSchema[type=object, properties={itemName={type=string}}, required=[itemName], additionalProperties=false, defs=null, definitions=null]
Tool: get_last_order_content_in_json, description: get last order content formatted in json, schema: JsonSchema[type=object, properties={}, required=[], additionalProperties=false, defs=null, definitions=null]
Tool: add_to_cart_item, description: Add to cart item by specifying item name and its quantity, schema: JsonSchema[type=object, properties={itemName={type=string}, quantity={type=integer, format=int32}}, required=[itemName, quantity], additionalProperties=false, defs=null, definitions=null]
Resource: store_rules, mcp://brandz/store/rules, return content of store rules in plain text
Resource: store_faqs, mcp://brandz/store/faqs, content of store faqs in plain text
result = ReadResourceResult[contents=[TextResourceContents[uri=mcp://brandz/store/rules, mimeType=text/plain, text=norules]]]
Prompt: brandz-greeting, Greets the user visiting Brand Z Sports Store
Prompt: country-status, Gives information on how many stores are there in the input country name
result = GetPromptResult[description=Brand Z Greeting, messages=[PromptMessage[role=ASSISTANT, content=TextContent[annotations=null, text=Hi Doe! Welcome to Brand Z Sports Store.]]]]
Completion = CompleteResult[completion=CompleteCompletion[values=[Afghanistan, Albania, Algeria, Andorra, Angola, Antigua and Barbuda, Argentina, Armenia, Australia, Austria, Azerbaijan], total=11, hasMore=false]]
generatedPrompt = GetPromptResult[description=null, messages=[PromptMessage[role=USER, content=TextContent[audience=null, priority=null, text=Please write a warm, friendly greeting for someone named Doe.]]]]
[2m2025-07-16T23:00:42.454+05:30[0;39m [33m WARN[0;39m [35m26828[0;39m [2m--- [mcp] [onPool-worker-1] [0;39m[36mi.m.c.transport.StdioClientTransport [0;39m [2m:[0;39m Process terminated with code 1
Above is the output of the MCP Client.
MCP Inspector
I will not spend much time on the MCP inspector. You can launch the inspector by running below.
npx @modelcontextprotocol/inspector java -jar D:/my-mcp-server-dist/mymcpserver-0.0.1-SNAPSHOT.jar.
You will need to have Node.js and npx in your path for this to work. Whether you do java-jar or java-cp, just keep in mind it need not be Java here. It's just any stdio MCP server process that can be launched this way.
Once launched, press the connect button, and you can observe the logs and play around with the MCP inspector. That said, this is just the MCP inspector driving the stdio MCP server. Normally, under stdio, that's all you can do with the MCP inspector.
When used with HTTP SSE, MCP servers launched by other MCP clients, the MCP inspector becomes much more useful.
Conclusion
This article was mainly about stdio MCP protocol logging and some tricks I explored in that area. I hope you find that useful.
I also went into various MCP concepts.
I also hope you can see how this type of application need not be just a console or IDE-limited application. Used with SSE, it should open doors to making various online/mobile applications much smarter or at least more conversational and agentic.
Complete Code
Complete code is available here.
Sample Tools, Resources, Prompts, and Completions
For sample MCP Artifacts, see here.
References
- https://modelcontextprotocol.io/sdk/java/mcp-server
- https://modelcontextprotocol.io/docs/concepts/prompts
- https://modelcontextprotocol.io/docs/concepts/resources
- https://modelcontextprotocol.io/docs/concepts/tools
- https://modelcontextprotocol.io/docs/concepts/sampling
- https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_resource_management
- https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html
- https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/mcp-annotations-server
- https://github.com/spring-ai-community/awesome-spring-ai
Acknowledgements
I would like to thank Dan Vega and Badr Nass Lahsen for reviewing this article of mine.
Opinions expressed by DZone contributors are their own.




Comments