DEV Community

Cover image for Building an MCP Server and Client with Spring AI and Ollama

Building an MCP Server and Client with Spring AI and Ollama

Main Technologies

  • Docker
  • Java 17
  • Spring Boot 4.0.6
  • Spring AI 1.0.0
  • Ollama

Introduction

Large Language Models (LLMs) are no longer limited to answering questions. They can also interact with external systems, retrieve information, and perform actions on behalf of users.

To make this possible, we need a standard way for models and applications to communicate. This is where the Model Context Protocol (MCP) comes in.

MCP is an open protocol that allows AI applications to expose and consume capabilities in a consistent way. Instead of building custom integrations for every model or framework, MCP provides a common language that different systems can understand.

An MCP server can expose three types of capabilities:

  • Tools → actions that can be executed.
  • Resources → data that can be read.
  • Prompts → reusable prompt templates.

For example:

  • A tool could fetch the weather from an API.
  • A resource could expose a document or configuration file.
  • A prompt could provide instructions for summarizing text or generating code.

By using MCP, applications become easier to extend, reuse, and integrate with different AI models.

What you will build

When you reach the end of this article you should have built

  • An MCP (Model Context Protocol) server that exposes tools and ways to list tools and test them with postman MCP requests, the client and server communicate using the SSE transport, implemented with Spring WebFlux.
  • An MCP client using the Ollama LLM model and Spring AI to connect to the MCP server tools via WebFlux and a way to test the Large Language Model (LLM) with postman, results will be streamed in real time.
  • A local docker deployment of Ollama using the granite4:3b model which allows tool executions and it is really lightweight.

Everything we will see in this article is in the following GitHub repository

https://github.com/jlcastrillon91/spring-ai-mcp-sample.git

Architecture Overview

Architecture diagram showing the communication flow between a User, a Spring AI MCP Client, a remote MCP Server, and a local Ollama LLM instance

Rest API / MCP Client

Spring Boot Rest API using Spring AI to connect to an LLM either deployed locally (with Ollama) or remotely with a cloud LLM provider (ex. Anthropic or OpenAI), it exposes an endpoint that given a prompt sends it to the LLM which is connected to the MCP server for remote tool execution.

MCP Server

The MCP Server is the main piece of the puzzle, it is also a spring boot application which key responsibility is to provide tools, prompts and resources to agents so that they can connect remotely. Each agent can connect to multiple MCP servers.

Ollama LLM

Ollama is a tool that allows you to run LLM locally on your machine. Instead of sending prompts to cloud providers, Ollama downloads and serves models directly from your computer, cool is’n it?

Configure local Ollama LLM

Create a docker-compose.yml file for local deployment of Ollama.

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: demo-ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
      - ./scripts/init-ollama.sh:/init-ollama.sh
    networks:
      - realestatecopilot-network
    restart: unless-stopped
    environment:
      - OLLAMA_HOST=0.0.0.0
    entrypoint: ["/bin/bash", "-c"]
    command: "/init-ollama.sh"

volumes:
  ollama_data:

networks:
  realestatecopilot-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Now we have to make sure the Ollama model is running localhost. The following script pulls and initialises the Ollama model, it is necessary that Ollama server is running before pulling the model.

#!/bin/bash

# Serve ollama in the background to pull the model properly
ollama serve &
# Wait for the server to be ready
sleep 5
# Pull the model
ollama pull granite4:3b
# Restart ollama server once the model has been pulled
pkill ollama && ollama serve
Enter fullscreen mode Exit fullscreen mode

Very Important!! Do not forget to make the script executable

chmod +x scripts/init-ollama.sh
Enter fullscreen mode Exit fullscreen mode

NOTE: We selected granite4:3b for two main reasons, it is lightweight and allows tool execution.

Now execute the docker container with docker-compose.

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

You can check that Ollama is running localhost just by executing in the console
curl [http://localhost:11434/api/tags](http://localhost:11434/api/tags), you must be able to get the following JSON response

{"models":[{"name":"granite4:3b","model":"granite4:3b","modified_at":"2026-06-15T12:13:11.272613013Z","size":2099521385,"digest":"89962fcc75239ac434cdebceb6b7e0669397f92eaef9c487774b718bc36a3e5f","details":{"parent_model":"/Users/runner/.ollama/models/blobs/sha256-6c02683809a8dc4eb05c78d44bc63bcd707703b078998fa58829c858ab337bb0","format":"gguf","family":"granite","families":["granite"],"parameter_size":"3.4B","quantization_level":"Q4_K_M","context_length":131072,"embedding_length":2560},"capabilities":["completion","tools"]}]}
Enter fullscreen mode Exit fullscreen mode

Building the MCP Server

The MCP Server is a very simple spring boot service. A good starting point for any spring boot application is to use official Spring Initializr at https://start.spring.io, You can specify your dependencies in the ¨Dependencies¨ section add Model Context Protocol Server dependency you can skip this part and add the Ollama dependency manually later, but it will configure everything you need for working with MCP server side.

Spring Initializr UI showing a Java Maven project configuration with the Spring AI Model Context Protocol Server dependency selected

Go to the build.gradle file and change org.springframework.ai:spring-ai-starter-mcp-server to org.springframework.ai:spring-ai-starter-mcp-server-webflux for using WebFlux transport, so your dependencies final version shall be.

dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webflux'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
Enter fullscreen mode Exit fullscreen mode

Run ./gradlew build to make sure all dependencies are fine, go ahead if you see ¨BUILD SUCCESSFUL¨ message in the console.

Now open your application.properties and use 8081 port for the server since we will be using 8080 for the client on this article, but it is up to you to use the port you like the most.

server.port=8081
Enter fullscreen mode Exit fullscreen mode

Creating tools

Let’s now create our first tool, create a class UndervaluedProperties.java and add this code with a mocked tool that will simulate fetching data from an external api.

package com.example.mcp_server.tools;

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class UndervaluedProperties {

    @McpTool(
            name = "detect_undervalued_properties",
            description = "Detect undervalued properties based on user criteria"
    )
    public OpportunitiesResponse detectUndervaluedProperties(

            @McpToolParam(
                    description = "Target city or area",
                    required = true
            )
            String location,

            @McpToolParam(
                    description = "Minimum budget",
                    required = true
            )
            int budgetMin,

            @McpToolParam(
                    description = "Maximum budget",
                    required = true
            )
            int budgetMax
    ) {

        Opportunity mockOpportunity = new Opportunity(
                "123",
                "https://www.idealista.com/inmueble/123456789/",
                87,
                12.4,
                6.8,
                List.of(
                        "15% below comps",
                        "High rental demand",
                        "Listed 110 days"
                )
        );

        return new OpportunitiesResponse(List.of(mockOpportunity));
    }

    public record OpportunitiesResponse(
            List<Opportunity> opportunities
    ) {
    }

    public record Opportunity(
            String listingId,
            String url,
            int score,
            double undervaluation,
            double estimatedYield,
            List<String> reason
    ) {
    }
}
Enter fullscreen mode Exit fullscreen mode

The org.springframework.ai.mcp.annotation.McpTool class decorates a method turning into a reusable tool that we can use from remote LLMs connected to the server using an MCP client, notice that the name of the tool is how it will be recognised later when listing tools, either the name and the description in conjunction with the specified system and user prompts will make possible that the LLM model decides to execute the tool, so make sure to be as specific as possible.

Another important aspect of a tool is org.springframework.ai.mcp.annotation.McpToolParam . Use this class to decorate each tool parameter and be as specific as possible in the descriptions, this will help the model capture and parse the necessary parameters from user.

IMPORTANT! Do not forget to declare your tool class as @Component so that spring ai starter auto-configuration registers properly the tool.

Creating Prompts

A really powerful way to level up your LLM is to add prompts, yes I know, you can have local prompts and pass them directly to your agent, but this way lacks of scalability and many times is quite unorganised. MCP allows you to specify prompts in a declarative way, so a prompt can have the following components.

  • Name: This is an identifier for the Prompt, just an ID.
  • Title: This one is a human-readable (or AI-readable 😀) name for the prompt for displaying it.
  • Description: The description is a human-readable text for the agent to know what the prompt does, this is crucial for the agent deciding if executing the prompt.
  • Arguments: A list of arguments that will allow your prompt to be reusable and extensible.

So let’s go for the code, we will create another @Component InvestorGreetingPrompt that will encapsulate

package com.example.mcp_server.prompts;

import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Role;
import org.springframework.ai.mcp.annotation.McpArg;
import org.springframework.ai.mcp.annotation.McpPrompt;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class InvestorGreetingPrompt {
    @McpPrompt(
            name = "investor_greeting",
            title = "Investor Greeting",
            description = "Greet the investor interested in investment opportunities in simple language")
    public McpSchema.GetPromptResult investorGreeting(
            @McpArg(name = "name", description = "User's name")
            String name) {

        // Build the Text Content with a personalized greeting message
        String message = "You are an investor assistant that will help the user [" + name + "], " +
                "when using the tool to detect undervalued properties. " +
                "Always provide clear and concise information about the investment opportunities, " +
                "and guide the user through the process of evaluating potential investments." +
                "Greet the user and thank him for his interest, after you show him the undervalued properties," +
                "ask him for his phone number to contact him for more information about the investment opportunities.";
        McpSchema.Content content = TextContent.builder(message).build();

        // Return the prompt result with the assistant's greeting message
        return McpSchema.GetPromptResult.builder(
                List.of(new McpSchema.PromptMessage(Role.ASSISTANT, content))
        ).build();
    }
}

Enter fullscreen mode Exit fullscreen mode

Use the @McpArg class to specify your prompt arguments, these will be the variables your prompt my have, in our case the name of the user. The @MCPArg class also have a required argument whose default value is false, in our case it means the name of the user is optional.

Testing the MCP Server

Now run the server ./gradlew run

If you see the following entries in the application logs it means your server is fine.

Registered tools: 1
Registered prompts: 1
Enter fullscreen mode Exit fullscreen mode

So this is all in the server side, simple isn’t it. Now let’s give it a test with Postman. Install and open Postman, create a new Request and select MCP type.

Screenshot of the Postman 'New Request' dialog highlighting the specific Model Context Protocol (MCP) request type option

paste http://localhost:8081/sse as the request url and voila! you can now test your tools and prompts, so start playing with it a bit.

Postman interface showing a successful SSE connection to the MCP server at localhost:8081, displaying registered tools and prompts

Building the MCP Client

The MCP Client will be a spring boot Rest API that will use Ollama local model to answer the prompt and execute tools from the MCP server. Open https://start.spring.io and specify your dependencies, in the Dependencies section add the following entries.

  • Ollama
  • Model Context Protocol Client
  • Spring Web

Spring Initializr to configure an MCP client with Ollama, MCP and Spring Web dependencies

Click on generate, unzip the downloaded file and open the project in your preferred IDE, we are using for this article https://www.jetbrains.com/idea/download.

Once the project opened make sure the project is configured and runs fine

./gradlew bootRun --args='--spring.profiles.active=dev'
Enter fullscreen mode Exit fullscreen mode

You must be able to see the logs meaning the project is running fine!

Terminal output showing successful Spring Boot application startup logs for the MCP client project

Now that we know the project is running fine let’s add some code 😀.

Go to the build.gradle file and change org.springframework.ai:spring-ai-starter-mcp-client to org.springframework.ai:spring-ai-starter-mcp-client-webflux for using WebFlux transport, so your dependencies final version shall be.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.ai:spring-ai-starter-mcp-client-webflux'
    implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
Enter fullscreen mode Exit fullscreen mode

Open the project configuration file src/main/resources/application.properties and append spring.ai.ollama configuration parameters.

spring.ai.ollama.base-url=http://127.0.0.1:11434
spring.ai.ollama.chat.options.model=granite4:3b
spring.ai.ollama.chat.options.temperature=0.1
Enter fullscreen mode Exit fullscreen mode

With these parameters Spring AI will know which LLM provider it will connect to, in our case the local Ollama model deployed earlier with docker.

Also add to the application.properties file the spring.ai.mcp properties to connect properly to the MCP server.

spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.sse.connections.mcp_server.url=http://localhost:8081
Enter fullscreen mode Exit fullscreen mode

The final version of your application.properties should look something like this

spring.application.name=mcp_client

spring.ai.ollama.base-url=http://127.0.0.1:11434
spring.ai.ollama.chat.options.model=granite4:3b
spring.ai.ollama.chat.options.temperature=0.1

spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.sse.connections.mcp_server.url=http://localhost:8081
Enter fullscreen mode Exit fullscreen mode

Creating the Prompt Service

Let us now create our first service PromptService.java with the following code.

package com.example.mcp_client.service;

import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import io.modelcontextprotocol.client.McpSyncClient;

import java.util.List;
import java.util.Map;

@Service
public class PromptService {

    private final ChatClient chatClient;
    private final ToolCallback[] tools;
    private final McpSyncClient mcpClient;

    public PromptService(
            SyncMcpToolCallbackProvider toolCallbackProvider,
            ChatClient.Builder chatClientBuilder,
            List<McpSyncClient> syncClients
    ) {
        this.chatClient = chatClientBuilder.build();
        this.tools = toolCallbackProvider.getToolCallbacks();
        // We only have one MCP client, so we can just get the first one from the list
        this.mcpClient = syncClients.get(0);
    }

    public Flux<String> sendPrompt(String userInput) {
        // Retrieve the prompt from the MCP server
        McpSchema.GetPromptRequest mcpPromptRequest = McpSchema.GetPromptRequest
                .builder("investor_greeting")
                .arguments(Map.of("name", "Joseph")) // This is optional
                .build();
        McpSchema.GetPromptResult promptResult = mcpClient.getPrompt(mcpPromptRequest);
        McpSchema.TextContent textContent = (McpSchema.TextContent)promptResult.messages().stream().findFirst().get().content();
        var prompt = textContent.text();
        return chatClient
                .prompt(prompt)
                .tools(this.tools)
                .user(userInput)
                .stream()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

The sendPrompt method receives a text from the user and sends it to the LLM, it enhances the LLM with the tools and prompts provided in the MCP server.

The org.springframework.ai.chat.client.ChatClient class encapsulates the behaviour of all LLM related operations.

The org.springframework.ai.mcp.SyncMcpToolCallbackProvider class allows to obtain the latest version of the tools from the configured MCP server.

The McpSchema.GetPromptRequest class allows you to obtain either resources or prompts from the MCP server. Notice syncClients in our case only is connected to one MCP server, if you have multiple MCP servers you need to filter by name.

Creating the Prompt Controller

Create a controller PromptController.java to make use of the previously created PromptService.

package com.example.mcp_client.controller;

import com.example.mcp_client.dto.PromptRequest;
import com.example.mcp_client.service.PromptService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/prompt")
public class PromptController {

    private final PromptService promptService;

    public PromptController(PromptService promptService) {
        this.promptService = promptService;
    }

    @PostMapping
    public Flux<String> sendPrompt(@RequestBody PromptRequest request) {
        return promptService.sendPrompt(request.message());
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that we are serving the endpoint as a Flux<String> so that it can only be consumed

Testing the MCP Client

Run your MCP Client ./gradlew bootRun --args='--spring.profiles.active=dev’ and send this request either with postman or simply using curl .

POST http://localhost:8080/prompt
Body:

{
"message": "Tell me the undervalued properties in Barcelona my budget is 100k to 300k, use available tools"
}
Enter fullscreen mode Exit fullscreen mode

You must receive a 200 response with the a text like the following one:

Hello Joseph, thank you for your interest in investing! Based on the tool's analysis of undervalued properties in Barcelona within your budget range of $100k to $300k, here are some promising opportunities:

1. **Property Listing**:  
   - **Listing ID**: 123  
   - **URL**: [Idealista Property](https://www.idealista.com/inmueble/123456789/)  
   - **Undervaluation Score**: 12.4% (indicating a strong potential for value appreciation)  
   - **Estimated Yield**: 6.8%  
   - **Reasons for Undervaluation**:  
     - 15% below comparable market prices  
     - High rental demand in the area  
     - Listed on the market for 110 days  

These properties show excellent potential due to their current low price relative to similar listings, strong rental interest, and stable market presence. The estimated yield of 6.8% suggests a solid return on investment.

Would you like more detailed information about any of these opportunities? Please provide your phone number so we can discuss further and answer any questions you may have.
Enter fullscreen mode Exit fullscreen mode

Notice that the response will be streamed back to the user.
So this is all, now you have everything up and running, make sure to start the MCP Server and the Ollama service before starting the MCP client.

Final Thoughts

In this article, we built a complete MCP-based application from scratch.

We created an MCP server capable of exposing tools and prompts, and an MCP client able to consume those capabilities through Spring AI. We also deployed a local Ollama model and enabled it to execute tools and stream responses back to the user.

Although the example is intentionally simple, the same architecture can be extended to more realistic scenarios, such as integrating databases, external APIs, vector stores, or even multiple MCP servers.

Hopefully this article helped demystify the Model Context Protocol and showed that building AI applications with Spring AI and MCP can be both powerful and surprisingly straightforward.

How are you planning to use MCP in your projects? Are there specific tools or APIs you're looking to expose to LLMs? Let's discuss in the comments!"

The complete source code is available on GitHub, so feel free to experiment with it and adapt it to your own projects.

spring-ai-mcp-sample

This is a simple Spring Boot project that demonstrates how to connect an MCP Server and MCP Client using Spring AI, WebFlux, and a local LLM (Ollama).


🚀 What this project does

This project shows how to build an AI system where:

  • A user sends a prompt
  • A local model (Ollama) processes the request
  • The model can request tools when needed
  • Tools are executed through an MCP Server
  • Spring AI connects everything together
  • WebFlux handles streaming communication

🧠 Main idea

The project is based on the Model Context Protocol (MCP).

MCP is a standard way for AI models to use external tools.

It defines three main concepts:

  • Tools → actions the model can execute
  • Resources → data the model can read
  • Prompts → reusable prompt templates

🏗️ Architecture

User
  ↓
Ollama (LLM)
  ↓
Spring AI (Orchestration Layer)
  ↓
MCP Client
  ↓
MCP Server (Tools, Prompts, Resources)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)