Tuesday, April 22, 2025

Building an MCP Server and MCP Client with Spring Boot and MongoDB

 This technical blog post demonstrates how to create a Model Context Protocol (MCP) server using Spring Boot that interacts with a MongoDB database. We will also build a Spring Boot MCP client to consume the services exposed by this server.

Setting up MongoDB

Before we begin, let's populate our local MongoDB instance with some initial data. Create a collection named siddhucollection and insert the data as shown in

Creating the MCP Server

Next, we will build a Spring Boot application that will function as our MCP server.

1. pom.xml

This file defines the project dependencies, including Spring Boot Web Starter, Spring AI MCP Server Starter, and Spring Boot Data MongoDB.

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

	<groupId>com.bootcamptoprod</groupId>
	<artifactId>spring-boot-ai-mongo-mcp-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>spring-boot-ai-mongo-mcp-server</name>
	<description>A Spring Boot AI-powered Model Context Protocol Server for interacting with MongoDB</description>

	<properties>
		<java.version>17</java.version>
		<spring-ai.version>1.0.0-M6</spring-ai.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-mongodb</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>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
        		<source>17</source>
        		<target>17</target>
    			</configuration>
				</plugin>
		</plugins>
	</build>

</project>

2. SpringBootAiMongoMcpServerApplication.java

This is the main entry point for our Spring Boot application.

Java

package com.siddhu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootAiMongoMcpServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootAiMongoMcpServerApplication.class, args);
	}

}

3. McpServerConfiguration.java

This configuration class defines a ToolCallbackProvider bean, which registers our MongoDB service as a tool accessible via MCP.

Java

package com.siddhu.config;

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;

import com.siddhu.service.MCPServerMongoServiceClient;

@Configuration
public class McpServerConfiguration {

    @Bean
    public ToolCallbackProvider mongoTools(MCPServerMongoServiceClient mongoServiceClient) {
        return MethodToolCallbackProvider.builder().toolObjects(mongoServiceClient).build();
    }
}

4. MCPServerMongoServiceClient.java

This service class contains the methods that interact with MongoDB. Each method annotated with @Tool will be exposed as a function callable through the MCP.

Java

package com.siddhu.service;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class MCPServerMongoServiceClient {

    private static final Logger logger = LoggerFactory.getLogger(MCPServerMongoServiceClient.class);
    private final MongoClient mongoClient;

    /**
     * Initializes the MongoDB client with the given URI.
     */
    public MCPServerMongoServiceClient(@Value("${mongodb.uri}") String mongoUri) {
        logger.info("Initializing MCPServerMongoServiceClient with URI: {}", mongoUri);
        this.mongoClient = MongoClients.create(mongoUri);
    }

    /**
     * Lists all databases in MongoDB.
     */
    @Tool(description = "Provide he list of all databases in MongoDB.")
    public List<String> listDatabases() {
        logger.info("Fetching list of databases.");
        List<String> databaseNames = new ArrayList<>();
        for (Document db : mongoClient.listDatabases()) {
            databaseNames.add(db.getString("name"));
        }
        logger.info("Databases found: {}", databaseNames);
        return databaseNames;
    }

    /**
     * Lists all collections in the specified database.
     */
    @Tool(description = "Provide the list of all collections in the specified database.")
    public List<String> listCollections(String dbName) {
        logger.info("Fetching collections for database: {}", dbName);
        List<String> collectionNames = new ArrayList<>();
        MongoDatabase database = mongoClient.getDatabase(dbName);
        for (String name : database.listCollectionNames()) {
            collectionNames.add(name);
        }
        logger.info("Collections found in {}: {}", dbName, collectionNames);
        return collectionNames;
    }   

    /**
     * Lists all indexes for a specific collection.
     */
    @Tool(description = "Provie the list of all the indexes for a specific collection.")
    public List<Document> listIndexes(String dbName, String collectionName) {
        logger.info("Fetching indexes for {}.{}", dbName, collectionName);
        MongoCollection<Document> collection = mongoClient.getDatabase(dbName).getCollection(collectionName);
        List<Document> indexes = new ArrayList<>();
        collection.listIndexes().into(indexes);
        logger.info("Indexes found: {}", indexes);
        return indexes;
    }

    /**
     * Creates a new collection in the specified database.
     */
    @Tool(description = "Create a new collection in the specified database.")
    public String createCollection(String dbName, String collectionName) {
        logger.info("Creating collection '{}' in database '{}'", collectionName, dbName);
        MongoDatabase database = mongoClient.getDatabase(dbName);
        database.createCollection(collectionName);
        logger.info("Collection '{}' created successfully.", collectionName);
        return "Collection '" + collectionName + "' created successfully in database '" + dbName + "'.";
    }

}

5. application.properties

This file configures the Spring Boot application, including the MongoDB connection URI and MCP server details. Replace ${MONGO_HOST} and ${MONGO_PORT} with your MongoDB instance's host and port.

Properties

spring.application.name=spring-boot-ai-mongo-mcp-server

spring.main.banner-mode=off
spring.main.web-application-type=none
logging.file.name=./logs/spring-boot-ai-mongo-mcp-server.log
logging.pattern.console=

spring.ai.mcp.server.name=mongo-mcp-server
spring.ai.mcp.server.version=0.0.1

# MongoDB connection string
mongodb.uri=mongodb://${MONGO_HOST}:${MONGO_PORT}

Now, build the server application and create the JAR file using the command shown in

Creating the MCP Client

Next, we will build a Spring Boot application that acts as an MCP client to interact with our MongoDB MCP server.

1. pom.xml

This file defines the dependencies for the client application, including Spring Boot Web Starter, Spring AI OpenAI Starter (or Ollama), and Spring AI MCP Client Starter.

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

	<groupId>com.bootcamptoprod</groupId>
	<artifactId>spring-boot-ai-mcp-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>spring-boot-ai-mcp-client</name>
	<description>A simple Spring Boot application for interacting with MCP servers using Spring AI Chat Client and Rest Controller</description>

	<properties>
		<java.version>17</java.version>
		<spring-ai.version>1.0.0-M6</spring-ai.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-ollama-spring-boot-starter</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>

2. SpringBootAiMcpClientApplication.java

This is the main entry point for our MCP client application.

Java

package com.siddhu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootAiMcpClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootAiMcpClientApplication.class, args);
	}

}

3. MCPClientChatClientConfig.java

This configuration class creates a ChatClient bean, which will be used to interact with the MCP server. It configures the client to use the available tools.

Java

package com.siddhu.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MCPClientChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
        return chatClientBuilder.defaultTools(tools).build();
    }
}

4. ChatController.java

This REST controller exposes an endpoint to send user input to the ChatClient, which will then interact with the MCP server.

Java

package com.siddhu.controller;


import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatClient chatClient;

    @Autowired
    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @PostMapping("/ask")
    public String chat(@RequestBody String userInput) {
        return chatClient.prompt(userInput).call().content();
    }
}

5. application.yml (for OpenAI)

This configuration file sets up the logging level, Spring application name, OpenAI API key, base URL, model, and the MCP client configuration to connect to our MongoDB MCP server. Replace <your_openrouter_api_key> with your actual API key.

YAML

logging:
  level:
    io:
      modelcontextprotocol:
        client: DEBUG
        spec: DEBUG
spring:
  application:
    name: spring-boot-ai-mcp-client
  ai:
    ollama:
      base-url: http://localhost:11434 # Default Ollama API base URL
      chat:
        options:
          model: qwen2.5-coder:1.5b     
#    openai:
#      api-key: "your_openai_api_key"
#      base-url: https://openrouter.ai/api
#      chat:
#        options:
#          model: google/gemini-2.0-flash-exp:free
    mcp:
      client:
        #stdio:
         # servers-configuration: classpath:mcp-servers-config.json
        stdio:
          connections:
            mongo-mcp-server:
              command: java
              args:
                - "-jar"
                - "C:\\STS-Workspace\\spring-boot-ai-mongo-mcp-server\\target\\spring-boot-ai-mongo-mcp-server-0.0.1-SNAPSHOT.jar"
              env:
                "MONGO_HOST": "localhost"
                "MONGO_PORT": "27017"    

Friday, April 18, 2025

Bridging the Gap: An Introduction to Google’s Agent-to-Agent (A2A) Protocol with Python Examples

 In the ever-evolving landscape of AI agents, seamless communication and interoperability are paramount. Imagine a scenario where one specialized agent needs to leverage the output of another to fulfill a user’s request. Without a standardized way for these agents to interact, we often find ourselves building intricate and brittle integration layers.

Consider this:

Agent 1: A service connected to a local Spring Boot application that provides the current temperature of a specific location.

Agent 2: An OpenAI-powered agent that takes the temperature as input and suggests activities suitable for that weather.

The challenge arises because the raw output of Agent 1 isn’t directly consumable by Agent 2. We’d typically need to manually transform the data into a format OpenAI understands. This becomes a significant headache when you need to swap out services or when underlying models are updated, forcing you to rewrite substantial portions of your communication logic. Remember the pain when OpenAI released a new model or when you wanted to experiment with Anthropic’s Claude? It often meant significant code rewrites in the communication layer.

To address this very problem, Google introduced the Agent-to-Agent (A2A) protocol. This protocol aims to standardize the way AI agents communicate, fostering a more flexible and robust ecosystem. Let’s dive into how we can use the python-a2a library to implement this.

Step 1: Installation

First things first, let’s install the necessary Python library:

Bash

1
pip install python-a2a

Step 2: Creating an Agent Server (Addition Example)

Let’s build a simple agent that exposes an “add” method. This agent will receive a text message containing two comma-separated numbers and return their sum.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# --- Modified AddAgent Server Code ---
from python_a2a import A2AServer, Message, TextContent, MessageRole, run_server
import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
class AddAgent(A2AServer):
    """
    An agent that expects a single text message containing two numbers
    separated by a comma (e.g., "5,2") and returns their sum.
    """
 
    # MODIFIED: handle_message now accepts ONE message argument
    def handle_message(self, message: Message):
        logging.info(f"Received message: {message}")
 
        # Check if it's a text message
        if not (hasattr(message, 'content') and message.content.type == "text"):
            warning_msg = "Error: Input must be a text message."
            logging.warning(warning_msg)
            return Message(
                content=TextContent(text=warning_msg),
                role=MessageRole.AGENT,
                parent_message_id=getattr(message, 'message_id', None),
                conversation_id=getattr(message, 'conversation_id', None)
            )
 
        # Get the text and try to parse it
        text_input = message.content.text
        logging.info(f"Attempting to parse text: '{text_input}'")
 
        try:
            # Split the text by a comma (or another delimiter you choose)
            parts = text_input.split(',')
            if len(parts) != 2:
                raise ValueError("Input text must contain exactly two numbers separated by a comma.")
 
            # Attempt conversion to float
            num1 = float(parts[0].strip()) # Use strip() to remove leading/trailing whitespace
            num2 = float(parts[1].strip())
 
            # Perform the addition
            result = num1 + num2
            response_text = f"The sum of {num1} and {num2} is: {result}"
            logging.info(f"Calculation successful: {response_text}")
 
            # Return the result message
            return Message(
                content=TextContent(text=response_text),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
        except ValueError as e:
            # Handle cases where splitting fails or text cannot be converted
            error_msg = f"Error processing input '{text_input}': {e}"
            logging.error(error_msg)
            return Message(
                content=TextContent(text=error_msg),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
        except Exception as e:
            error_msg = f"An unexpected error occurred: {e}"
            logging.exception(error_msg) # Log full traceback
            return Message(
                content=TextContent(text="An internal server error occurred."),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
# Run the server (no changes needed here)
if __name__ == "__main__":
    agent = AddAgent()
    print("Starting AddAgent server...")
    # Now the handle_message signature matches what run_server expects
    run_server(agent, host="localhost", port=5000)

Save this code as a2a_add_server.py and run it. You should see the server starting on http://localhost:5000/a2a.

Step 3: Creating Another Agent Server (Subtraction Example)

Let’s create another agent, this time for subtraction, running on a different port.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# --- Modified AddAgent Server Code ---
from python_a2a import A2AServer, Message, TextContent, MessageRole, run_server
import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
class SubstractAgent(A2AServer):
    """
    An agent that expects a single text message containing two numbers
    separated by a comma (e.g., "5,2") and returns their Substraction.
    """
 
    # MODIFIED: handle_message now accepts ONE message argument
    def handle_message(self, message: Message):
        logging.info(f"Received message: {message}")
 
        # Check if it's a text message
        if not (hasattr(message, 'content') and message.content.type == "text"):
            warning_msg = "Error: Input must be a text message."
            logging.warning(warning_msg)
            return Message(
                content=TextContent(text=warning_msg),
                role=MessageRole.AGENT,
                parent_message_id=getattr(message, 'message_id', None),
                conversation_id=getattr(message, 'conversation_id', None)
            )
 
        # Get the text and try to parse it
        text_input = message.content.text
        logging.info(f"Attempting to parse text: '{text_input}'")
 
        try:
            # Split the text by a comma (or another delimiter you choose)
            parts = text_input.split(',')
            if len(parts) != 2:
                raise ValueError("Input text must contain exactly two numbers separated by a comma.")
 
            # Attempt conversion to float
            num1 = float(parts[0].strip()) # Use strip() to remove leading/trailing whitespace
            num2 = float(parts[1].strip())
 
            # Perform the addition
            result = num1 - num2
            response_text = f"The Substraction of {num1} and {num2} is: {result}"
            logging.info(f"Calculation successful: {response_text}")
 
            # Return the result message
            return Message(
                content=TextContent(text=response_text),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
        except ValueError as e:
            # Handle cases where splitting fails or text cannot be converted
            error_msg = f"Error processing input '{text_input}': {e}"
            logging.error(error_msg)
            return Message(
                content=TextContent(text=error_msg),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
        except Exception as e:
            error_msg = f"An unexpected error occurred: {e}"
            logging.exception(error_msg) # Log full traceback
            return Message(
                content=TextContent(text="An internal server error occurred."),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
# Run the server (no changes needed here)
if __name__ == "__main__":
    agent = SubstractAgent()
    print("Starting SubstractAgent server...")
    # Now the handle_message signature matches what run_server expects
    run_server(agent, host="localhost", port=5001)

Save this as a2a_substract_server.py and run it in a separate terminal. This server will start on http://localhost:5001/a2a.

Step 4: Creating an A2A Client

Now, let’s create a client that can communicate with both of these agents using the A2A protocol.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from python_a2a import A2AClient, Message, TextContent, MessageRole
 
# Create a client to talk to our addition agent
client_add = A2AClient("http://localhost:5000/a2a")
# Send a message
message_add = Message(
    content=TextContent(text="5,2"),
    role=MessageRole.USER
)
response_add = client_add.send_message(message_add)
# Print the response
print(f"Add Agent says: {response_add.content.text}")
 
 
# Create a client to talk to our subtraction agent
client_sub = A2AClient("http://localhost:5001/a2a")
# Send a message
message_sub = Message(
    content=TextContent(text="5,2"),
    role=MessageRole.USER
)
response_sub = client_sub.send_message(message_sub)
# Print the response
print(f"Sunstract Agent says: {response_sub.content.text}")

Save this code as a2a_client_agent_add_substract.py and execute it. You should see the following output:

1
2
Add Agent says: The sum of 5.0 and 2.0 is: 7.0
Sunstract Agent says: The Substraction of 5.0 and 2.0 is: 3.0

Conclusion

This simple example demonstrates the fundamental principles of the A2A protocol using the python-a2a library. By standardizing the message format and communication layer, A2A paves the way for more modular, maintainable, and interoperable AI agent systems. When you need to swap out an underlying service or integrate with a new model, the A2A protocol helps abstract away the low-level communication details, reducing the need for extensive code modifications.

You can find the complete code for this example on GitHub: https://github.com/shdhumale/A2AServerClient.git

As the field of AI agents continues to mature, protocols like A2A will be crucial in building complex and collaborative AI ecosystems. By embracing these standards, we can move towards a future where AI agents can seamlessly work together to solve intricate problems.