In this blog, you will learn how to create a Model Context Protocol (MCP) server using Spring AI. You will see with how little effort you can create your own MCP server. Enjoy!

1. Introduction

Model Context Protocol provides a standardized way to connect Large Language Models (LLMs) to different kind of data sources and tools. The word standardized is very important in this sentence. This means that integration with data sources and tools becomes much more easy than before. Besides that, MCP servers enhance your LLM with extra knowledge or extra functionality making it an even more powerful assistant. Imagine you can ask an LLM to book a holiday for you. Based on your preferences, it will search the internet for suitable locations, book a hotel, book a flight, etc. and of you go! An LLM would need to be able to book the hotel and flight for you, of course. This additional functionality can be provided by MCP servers. This sounds appealing but also scary. In order to book a hotel, the MCP server would need to know your personal details and would need access to your credit card. The might not be a very good idea. It is advised to use a human-in-the-loop (HITL) for sensitive actions so that you can approve or reject a certain action. However, MCP servers will make your life much easier.

In this blog, you will learn how to create your own MCP server using Spring Boot and Spring AI. A server does nothing without an MCP client. An MCP client interacts with one or more MCP servers and is the one that is in control. As MCP client you will (mis)use the IntelliJ DevoxxGenie plugin. DevoxxGenie is actually an AI coding assistant, but you can also use it to test your MCP servers. In a next blog, you will create your own MCP client.

The sources used in this blog are available at GitHub in the server directory.

2. Prerequisites

Prerequisites for reading this blog are:

  • Basic Java knowledge;
  • Basic Spring Boot knowledge;
  • Basic LMStudio knowledge;
  • Basic IntelliJ and DevoxxGenie knowledge.

3. Build MCP Server

The MCP server you will build, has the following functionality:

  • Return a list of my favorite artists;
  • Return a list of my favorite songs.

An LLM will not have any knowledge about this information and when it has access to these tools, it will hopefully make use of them. This application is heavily inspired by Creating Your First Model Context Protocol (MCP) Server in Java by Dan Vega.

Navigate to Spring Initializr and add dependency Model Context Protocol Server. This will add the following dependency to the pom.

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

Create the datamodel for Artist.

public record Artist(String name) {
}

Create the datamodel for Song.

public record Song(Artist artist, String title) {
}

Create the ArtistService. This service will contain a list of my favorite artists, but in a real world application, this would be stored in a database. A tool get_artists is defined by annotating it with @Tool. Give the tool a name and a description. The description will be used by an LLM in order to obtain knowledge about the capabilities of the tool.

@Service
public class ArtistService {

    private final List<Artist> artists = new ArrayList<>();

    @Tool(name = "get_artists", description = "Get the complete list of Gunter's favorite artists")
    public List<Artist> getArtists() {
        return artists;
    }

    @PostConstruct
    public void init() {
        artists.addAll(List.of(
                new Artist("Bruce Springsteen"),
                new Artist("JJ Johnson")
        ));
    }

}

In a similar way, you create the SongService.

@Service
public class SongService {

    private final List<Song> songs = new ArrayList<>();

    @Tool(name = "get_songs", description = "Get the complete list of Gunter's favorite songs")
    public List<Song> getSongs() {
        return songs;
    }

    @PostConstruct
    public void init() {
        songs.addAll((List.of(
                new Song(new Artist("Bruce Springsteen"), "My Hometown"),
                new Song(new Artist("JJ Johnson"), "Lament")
        )));
    }

}

You register the tools by means of a Bean.

@SpringBootApplication
public class MyMcpServerPlanetApplication {

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

	@Bean
	public ToolCallbackProvider mcpServices(ArtistService artistService, SongService songService) {
		return MethodToolCallbackProvider.builder()
				.toolObjects(artistService,	songService)
				.build();
	}

}

Finally, add the following application.properties.

spring.main.web-application-type=none
spring.ai.mcp.server.name=mcp-server
spring.ai.mcp.server.version=0.0.1

spring.main.banner-mode=off
logging.pattern.console=

This configuration does several important things:

  • Disable the web application: since STDIO transport is used for MCP, no web server is need.
  • Set the server name and version: this identifies the MCP server to clients.
  • Disable the banner and console logging: this is critical for STDIO transport to work correctly.

Build the jar file.

mvn clean verify

This results in a jar-file in the target directory: target/mcp-server-0.0.1-SNAPSHOT.jar.

4. Test MCP Server

In order to test the MCP server, an MCP client is necessary. As mentioned before, DevoxxGenie will be used for this purpose. See a previous post when you want to learn more about DevoxxGenie in combination with MCP.

Add the MCP server to the DevoxxGenie MCP settings. Do note that you need to use full paths for the command and arguments.

  • Name: MyMcpServerPlanet
  • Transport Type: STDIO
  • Command: /<java installation directory>/bin/java
  • Arguments: -jar /home/<project directory>/mymcpserverplanet/server/target/mymcpserverplanet-0.0.1-SNAPSHOT.jar

Click the Test Connection & Fetch Tools button. The caption of the button should change into Connection Successful! 2 tools found.

Together with DevoxxGenie, LMStudio is used as inference engine with model qwen3-8b running on a GPU.

Enter the prompt: give me a list of gunter’s favorite artists

The request is sent to LMStudio and also the available tools are sent in the request. When you have the debug logs enabled in LMStudio, you can check the request.

2025-07-20 11:45:42 [DEBUG]
 Received request: POST to /v1/chat/completions with body  {
  "model": "qwen3-8b",
  "messages": [
    {
      "role": "system",
      "content": "You are a software developer IDEA plugin with expe... <Truncated in logs> ...at is correct and relevant to the code or plugin.\n"
    },
    {
      "role": "user",
      "content": "<ProjectPath>\n/home/<project directory>... <Truncated in logs> ... list of gunter's favorite artists\n</UserPrompt>\n\n"
    }
  ],
  "temperature": 0,
  "top_p": 0.9,
  "stream": false,
  "max_tokens": 8000,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_songs",
        "description": "Get the complete list of Gunter's favorite songs",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "get_artists",
        "description": "Get the complete list of Gunter's favorite artists",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    }
  ]
}

The LLM will realize that it cannot answer this question by itself. However, the LLM does recognize that a tool is available which can answer the question. Therefore, the LLM will ask the client to invoke the MCP tool. DevoxxGenie has a built-in human-in-the-loop and will ask for your approval.

Click the Approve button. The MCP server will execute the get_artists tool and the response is sent back to the LLM. The LLM will use this response in order to give an appropriate answer.

The following response is received and is entirely correct. It is also interesting to see the thinking process the LLM gone through.

<think> Okay, the user asked for a list of Gunter's favorite artists. I called the \`get\_artists\` function and got back two artists: Bruce Springsteen and JJ Johnson. Now they might want to know if there are more or need further help.

I should confirm the results and maybe ask if there's anything else they need. Since the previous response was successful, it's good to keep that positive tone. No errors in the tool call, so no need for additional function calls here. </think>

Here are Gunter's favorite artists:

*   **Bruce Springsteen**
*   **JJ Johnson**

Let me know if you'd like more information or assistance!

Let’s check the get_songs tool with the following prompt: give me a list of gunter’s favorite songs

The same process is followed, the response is again correct.

<think> Okay, the user asked for a list of Gunter's favorite songs. I used the get\_songs function which retrieved two songs. Now I need to present this information clearly. Let me format each song with the artist and title. Make sure the response is easy to read, maybe bullet points. Check if there's any additional info needed, but since the user just wanted a list, keeping it simple should be best. </think>

Here is the list of Gunter's favorite songs:

1.  **Bruce Springsteen** - *My Hometown*
2.  **JJ Johnson** - *Lament*

Let me know if you'd like more details about any of these tracks!

5. Add Search Functionality

Let’s add some basic search functionality to both MCP tools

Add a search_artist tool to the ArtistService.

@Tool(name = "search_artist", description = "Search a single artist from Gunter's favorite artists")
public Artist getArtist(String name) {
    return artists.stream()
            .filter(artist -> artist.name().equalsIgnoreCase(name))
            .findFirst()
            .orElse(null);
}

Add a search_song tool to the SongService.

@Tool(name = "search_song", description = "Search a single song from Gunter's favorite songs")
public Song getSong(String title) {
    return songs.stream()
            .filter(song -> song.title().equalsIgnoreCase(title))
            .findFirst()
            .orElse(null);
}

Build the jar and fetch the new tools with DevoxxGenie.

Enter the following prompt: is bruce springsteen one of gunter’s favorite artists?

This time the search_artist tool is invoked and again the response is correct.

<think> Okay, the user asked if Bruce Springsteen is one of Gunter's favorite artists. I used the search\_artist function with his name. The response came back with "Bruce Springsteen" as an artist. Now I need to check if he's in Gunter's favorites.

Wait, the functions available are get\_artists and search\_artist. The search\_artist function probably checks if the artist exists in the database. Since the response returned the artist, that means Bruce Springsteen is indeed in Gunter's favorite artists list.

So the answer should be yes. I should confirm that based on the function's response. No need for further actions since the user's question is answered. Just present the result clearly. </think>

Yes, Bruce Springsteen is one of Gunter's favorite artists. The search confirmed his presence in the collection.

Enter the following prompt: are the rolling stones one of gunter’s favorite artists

The search_artist tool is invoked and since the Rolling Stones could not be found, the LLM decides that they are not in my favorite artists list.

<think> Okay, let me try to figure out what's going on here. The user asked if the Rolling Stones are one of Gunter's favorite artists. I used the search\_artist function with the name "Rolling Stones", but the response was null. That means the function didn't find any artist by that name in Gunter's favorites.

Hmm, maybe the artist's name isn't exactly "Rolling Stones" in the database. Could there be a typo or a different spelling? Or perhaps they're known by another name in the system. Alternatively, maybe the function isn't returning any results because the data isn't there.

Since the user is asking about Gunter's favorites, I should check if there's a way to list all favorite artists first. The get\_artists function could help here. If I call that, maybe I can see if the Rolling Stones are listed. But wait, the user might not want all artists, just confirmation on this specific one.

Alternatively, maybe the search\_artist function requires a different parameter. Let me check the tool definitions again. The search\_artist function takes a "name" parameter, which is required. So the query should be case-insensitive? Or maybe there's a partial match? The user might have meant "Rolling Stones" but the database has it stored differently.

Another thought: perhaps the Rolling Stones aren't in Gunter's favorite artists list. The search returned null, so maybe they aren't. But I should confirm by checking all artists first. Let me suggest that to the user. Wait, but the user is asking specifically about the Rolling Stones. Maybe I should inform them that the search didn't find any and ask if they want to check all artists instead.

Wait, the response was null, so the function didn't return any data. That means the artist isn't in the list. So the answer would be no, the Rolling Stones aren't one of Gunter's favorite artists. But maybe there's a mistake in the function call? Let me double-check. The function is called search\_artist with name "Rolling Stones". If the database has the exact name, then it should return. If not, then null.

So the conclusion is that the Rolling Stones aren't in Gunter's favorites. Alternatively, maybe there's a typo. But since the user is asking directly, I should answer based on the function's response. </think>

The search for the artist "Rolling Stones" did not return any results in Gunter's favorite artists list. This suggests that the Rolling Stones are **not** currently listed as one of Gunter's favorite artists. Would you like to check the full list of his favorite artists instead?

6. Conclusion

In this blog, you learnt how to create an MCP server using Spring Boot and Spring AI. Creating an MCP server and adding tools is very easy to do. You can focus on the functionality of the tool and that is exactly what you want.


Discover more from

Subscribe to get the latest posts sent to your email.