Friday, 17 July 2026

Build a Production-Ready MCP Server with Spring AI: Tools, Resources & Prompts Explained from Scratch

  

Building an MCP server is surprisingly straightforward once you understand the core concepts. Spring AI abstracts away much of the protocol specific plumbing, allowing developers to focus on implementing business capabilities instead of handling low-level JSON-RPC requests, protocol negotiation, or schema generation.

 

In this post, we'll build a complete Employee Management MCP Server using Spring Boot and Spring AI. Instead of exposing a single demo tool, we'll create a realistic server that demonstrates all three MCP capabilities:

 

·      Tools: to execute business operations such as searching employees, retrieving organizational hierarchy, and generating workforce statistics.

·      Resources: to expose structured, read-only organizational data through resource URIs.

·      Prompts: to provide reusable AI instructions, personas, and dynamic prompt templates that help AI clients generate richer and more consistent responses.

 

To keep the implementation focused on MCP concepts, the application uses an in-memory dataset of 100 employees distributed across multiple countries, departments, and management levels. This eliminates database setup while still providing enough realistic data to demonstrate filtering, pagination, hierarchical relationships, and AI interactions.

 

By the end of this guide, you'll have a fully functional stateless MCP server capable of exposing:

 

·      15 MCP Tools

·      15 MCP Resources

·      7 MCP Prompts

 

Along the way, we'll understand how Spring AI automatically discovers and registers these capabilities, how they are exposed to MCP clients, and how AI agents interact with them through the Model Context Protocol.

 

Rather than treating this as a toy example, we'll follow the same project structure, coding practices, and registration patterns that can be applied when building enterprise-grade MCP servers.

 

Follow below step-by-step procedure to build the complete working application.

 

Step 1: Create new maven project ‘employee-mcp’.

mvn archetype:generate \
  -DgroupId=com.sample.app \
  -DartifactId=employee-mcp \
  -Dversion=1.0.0 \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.5 \
  -DinteractiveMode=false

   

This will generate the following project structure:

 

employee-mcp
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── sample
    │               └── app
    │                   └── App.java
    └── test
        └── java
            └── com
                └── sample
                    └── app
                        └── AppTest.java

   

Feel free to remove src/test/java folder, as we are not going to focus on writing testcases.

 

Since we are building a Spring Boot + Spring AI MCP Server, this archetype only creates a basic Maven project. In the next step, we'll replace the generated pom.xml with a Spring Boot project configuration, add the required Spring AI MCP dependencies, and create the standard Spring Boot directory structure.

 

Step 2: Update pom.xml with Spring Boot + Spring AI specific dependencies.

 

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>

  <groupId>com.sample.app</groupId>
  <artifactId>employee-mcp</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <name>employee-mcp</name>
  <description>
    Employee MCP Server  A complete educational project demonstrating how
    to build
    an MCP (Model Context Protocol) Server using Spring AI. Covers Tools,
    Resources,
    Prompts, Pagination, Validation, and Error Handling with an in-memory
    employee dataset.
  </description>

  <properties>
    <!-- Java 21 bytecode target. Homebrew Maven runs on JDK 23 and cannot
         set release 24 (TypeTag incompatibility). The JAR runs fine on Java 24+;
         no Java 24-specific language features are used in this project. -->
    <java.version>21</java.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.5.3</spring-boot.version>
    <spring-ai.version>1.1.2</spring-ai.version>
  </properties>

  <!-- ═══════════════════════════════════════════════════════════════════════
         Dependency Management — Spring Boot BOM first, then Spring AI BOM.
         The Spring AI BOM manages all spring-ai artifact versions, including
         the transitive MCP SDK (mcp-core) version.
         ═══════════════════════════════════════════════════════════════════════ -->
  <dependencyManagement>
    <dependencies>
      <!-- Spring Boot BOM — manages Spring Framework, Jackson, SLF4J,
      etc. -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <!-- Spring AI BOM — manages all spring-ai-* artifact versions -->
      <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>

    <!-- ─── Spring AI MCP Server (WebMVC / Stateless HTTP) ───────────────
             This starter auto-configures the MCP server endpoint at POST /mcp.
             Uses STATELESS protocol: every request is a plain POST → response.
             No SSE sessions needed — works with any HTTP load balancer.
             ─────────────────────────────────────────────────────────────────── -->
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>

    <!-- ─── Spring Boot Actuator ─────────────────────────────────────────
             Exposes /actuator/health, /actuator/info management endpoints.
             ─────────────────────────────────────────────────────────────────── -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>


    <!-- ─── Configuration Processor ─────────────────────────────────────
             Generates IDE metadata for @ConfigurationProperties classes.
             ─────────────────────────────────────────────────────────────────── -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- ─── Test ─────────────────────────────────────────────────────────
             JUnit 5 + Spring Boot Test support.
             ─────────────────────────────────────────────────────────────────── -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

  </dependencies>

  <build>
    <plugins>

      <!-- Maven Compiler Plugin — targets Java 24 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.0</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
          <!-- Required so Spring AI can read parameter names via
          reflection -->
          <parameters>true</parameters>
          <annotationProcessorPaths>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <version>${lombok.version}</version>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>

      <!-- Spring Boot Maven Plugin — creates executable fat JAR -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <!-- Exclude Lombok from final JAR — only needed at compile
          time -->
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>

      <!-- Maven Surefire Plugin — runs JUnit 5 tests -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.3</version>
      </plugin>

    </plugins>
  </build>

</project>

Step 3: Create application.yml file in src/main/resources.

 

application.yml  

spring:
  application:
    name: employee-mcp

  ai:
    mcp:
      server:
        # Server identity — shown to MCP clients during negotiation
        name: employee-mcp
        version: "1.0.0"

        # STATELESS = plain HTTP POST /mcp → response in body.
        # Works with any HTTP load balancer. No SSE sessions.
        # The reference project (gdp-connect-mcp) also uses STATELESS.
        protocol: STATELESS

        # Capabilities advertised to MCP clients
        capabilities:
          tool: true       # @Tool methods in EmployeeTools
          resource: true   # MCP Resources (employees://*)
          prompt: true     # MCP Prompts (employee-summary, etc.)

        # Instructions shown to the AI client at session start
        instructions: |
          This MCP server exposes an Employee Management dataset with ~100 employees
          across multiple countries, departments, and management levels.

          Available capabilities:
            Tools (15):
              - getEmployees            : Paginated employee list
              - getEmployee             : Single employee by ID
              - getEmployeesUnderManager: All direct reports of a manager
              - getEmployeesByCity      : Paginated employees in a city
              - getEmployeesByCountry   : Paginated employees in a country
              - getEmployeesByDepartment: Paginated employees in a department
              - searchEmployees         : Keyword search across name/designation/dept
              - getManagers             : All employees who have direct reports
              - getDepartments          : All unique department names
              - getCountries            : All unique country names
              - getCities               : All unique city names
              - countEmployees          : Total / active / inactive counts
              - countEmployeesByCountry : Employee count per country
              - countEmployeesByDepartment: Employee count per department
              - getOrganizationHierarchy: Full org tree (CEO → VP → Director → …)

            Resources (15):
              employees://all, employees://countries, employees://cities,
              employees://departments, employees://organization, employees://managers,
              employees://statistics, employees://engineering, employees://sales,
              employees://finance, employees://hr, employees://marketing,
              employees://support, employees://india, employees://usa

            Prompts (7):
              employee-summary, department-summary, country-summary,
              manager-summary, hr-assistant, employee-search-assistant,
              organization-explorer

          Pagination is 0-indexed (pageNumber starts at 0).

# ─── Actuator endpoints ───────────────────────────────────────────────────────
management:
  endpoints:
    web:
      exposure:
        include: health, info
  endpoint:
    health:
      show-details: always
  info:
    env:
      enabled: true

# ─── App info (visible via /actuator/info) ────────────────────────────────────
info:
  app:
    name: Employee MCP Server
    version: "1.0.0"
    description: Educational MCP Server built with Spring AI

# ─── Logging ──────────────────────────────────────────────────────────────────
logging:
  level:
    com.sample.app: DEBUG
    org.springframework.ai.mcp: INFO
    org.springframework.ai.tool: INFO

server:
  port: 8080

   

Once the project dependencies are in place, the next step is configuring the MCP server. Spring AI provides auto-configuration for the server, and most of the configuration is done in the application.yml file.

 

Let's understand each section in detail.

 

Application Name

spring:
  application:
    name: employee-mcp

   

Every Spring Boot application has a name. This name is primarily used for identification in logs, monitoring tools, and Spring Boot Actuator. In our case, the application is named employee-mcp. Although this property is not specific to MCP, it is a standard Spring Boot configuration.

 

MCP Server Configuration

The most important configuration begins under:

spring:
  ai:
    mcp:
      server:

   

Everything inside this section controls how Spring AI creates and exposes the MCP server.

 

Server Identity

name: employee-mcp
version: "1.0.0"

   

Every MCP server advertises its identity when an AI client connects. During the initialization phase, the client learns basic information about the server, such as:

 

·      Server name

·      Server version

·      Supported capabilities

 

For our project:

·      Name: employee-mcp

·      Version: 1.0.0

 

This helps AI clients to identify which server they are interacting with, especially when multiple MCP servers are available.

        

Choosing the Protocol

protocol: STATELESS

   

This is one of the most important MCP configuration properties. Spring AI currently supports different communication models, and this project uses the STATELESS protocol.

 

With a stateless server:

 

·      Every request is completely independent

·      The server does not maintain client sessions

·      Each request contains all the information required for processing

 

The communication flow looks like this:

AI Client
     │
HTTP POST /mcp
     │
Process Request
     │
Return Response

   

After sending the response, the request is complete.

 

Advantages of Stateless MCP

·      Simple architecture

·      Easy to deploy

·      Works behind load balancers

·      Supports horizontal scaling

·      No session management

 

For most enterprise applications, a stateless MCP server is the recommended approach.

 

Advertised Capabilities

 

capabilities:
  tool: true
  resource: true
  prompt: true

   

These flags tell MCP clients which capabilities the server supports. Our server exposes all three MCP capability types.

 

tool: true

Enables support for MCP Tools. Spring AI automatically discovers every method annotated with @Tool and exposes it as an executable capability.

 

Examples from our project include:

·      getEmployee()

·      searchEmployees()

·      getEmployeesByCountry()

 

Resources

resource: true

 

Enables MCP Resources. Resources expose read-only data using URI-based identifiers such as:

 

·      employees://all

·      employees://statistics

·      employees://countries

 

Unlike tools, resources are intended for reading information rather than executing business operations.

 

Prompts

prompt: true

 

Enables MCP Prompts. Prompts provide reusable instructions that guide AI behaviour. Our project includes prompts such as:

 

·      employee-summary

·      department-summary

·      hr-assistant

·      organization-explorer

 

These prompts help the AI to generate richer and more consistent responses.

 

Server Instructions

instructions: |

 

This property is unique to MCP servers and is one of the most valuable configuration options. The instructions act as documentation for AI clients.

 

When an AI client connects to the server, these instructions are shared during initialization so the model immediately understands:

 

·      what the server does

·      what tools are available

·      what resources exist

·      what prompts can be used

·      and any important usage guidelines.

 

In our project, the instructions describe:

 

·      the Employee Management dataset

·      the available tools

·      the available resources

·      the available prompts

·      and the pagination rules

 

Instead of forcing the AI to discover everything through trial and error, the instructions provide a clear overview of the server's capabilities. Think of this as the user guide for the AI client.

 

A well written instructions section significantly improves the quality of tool selection and AI interactions.

 

With this configuration in place, Spring AI automatically initializes the MCP server, exposes the /mcp endpoint, advertises the server capabilities to connecting clients, and is ready to host the Tools, Resources, and Prompts that we'll implement in the following sections.

 

Step 4: Let’s define model classes like Employee, EmployeeCount, PagedResponse, and CountByGroup classes.

 

Employee.java

package com.sample.app.model;

import java.time.LocalDate;

/**
 * Core domain model representing an Employee. Every field maps directly to what
 * an HR system would store. The {@code managerId} forms a self-referencing
 * hierarchy:
 *
 * <pre>
 *   CEO (managerId = null)
 *     └── VP (managerId = CEO.id)
 *           └── Director (managerId = VP.id)
 *                 └── Manager (managerId = Director.id)
 *                       └── Employee (managerId = Manager.id)
 * </pre>
 *
 * <p>
 * Jackson serializes this as JSON automatically when Spring AI converts tool
 * results.
 */
public class Employee {

	/** Unique numeric identifier. Primary key. */
	private Long id;

	/** Human-readable employee number (e.g., "EMP-0042"). */
	private String employeeNumber;

	/** First name. */
	private String firstName;

	/** Last name. */
	private String lastName;

	/** Computed full name: "{firstName} {lastName}". */
	private String fullName;

	/** Corporate email address. */
	private String email;

	/** Phone number in international format. */
	private String phone;

	/** Job title (e.g., "Senior Engineer", "Director", "VP"). */
	private String designation;

	/** Business unit (Engineering, Finance, Sales, HR, etc.). */
	private String department;

	/** City of the employee's office. */
	private String city;

	/** State / Province. */
	private String state;

	/** Country of the employee's office. */
	private String country;

	/**
	 * ID of this employee's direct manager. {@code null} for the CEO (top of
	 * hierarchy).
	 */
	private Long managerId;

	/** Full name of the direct manager (denormalized for convenience). */
	private String managerName;

	/** Annual gross salary in USD. */
	private Double salary;

	/** Date the employee joined the company. */
	private LocalDate joiningDate;

	/**
	 * {@code true} if currently employed; {@code false} if on leave or terminated.
	 */
	private boolean active;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getEmployeeNumber() {
		return employeeNumber;
	}

	public void setEmployeeNumber(String employeeNumber) {
		this.employeeNumber = employeeNumber;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getFullName() {
		return fullName;
	}

	public void setFullName(String fullName) {
		this.fullName = fullName;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public String getDesignation() {
		return designation;
	}

	public void setDesignation(String designation) {
		this.designation = designation;
	}

	public String getDepartment() {
		return department;
	}

	public void setDepartment(String department) {
		this.department = department;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public Long getManagerId() {
		return managerId;
	}

	public void setManagerId(Long managerId) {
		this.managerId = managerId;
	}

	public String getManagerName() {
		return managerName;
	}

	public void setManagerName(String managerName) {
		this.managerName = managerName;
	}

	public Double getSalary() {
		return salary;
	}

	public void setSalary(Double salary) {
		this.salary = salary;
	}

	public LocalDate getJoiningDate() {
		return joiningDate;
	}

	public void setJoiningDate(LocalDate joiningDate) {
		this.joiningDate = joiningDate;
	}

	public boolean isActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

}

EmployeeCount.java

package com.sample.app.model;

/**
 * Summary count DTO returned by the {@code countEmployees} tool.
 *
 * <p>
 * Demonstrates how to return structured aggregation results from MCP tools
 * instead of raw numbers. AI clients can present these as a formatted summary.
 */
public class EmployeeCount {

	/** Total number of employees (active + inactive). */
	private long totalEmployees;

	/** Number of currently active employees. */
	private long activeEmployees;

	/** Number of inactive employees (on leave or terminated). */
	private long inactiveEmployees;

	public long getTotalEmployees() {
		return totalEmployees;
	}

	public void setTotalEmployees(long totalEmployees) {
		this.totalEmployees = totalEmployees;
	}

	public long getActiveEmployees() {
		return activeEmployees;
	}

	public void setActiveEmployees(long activeEmployees) {
		this.activeEmployees = activeEmployees;
	}

	public long getInactiveEmployees() {
		return inactiveEmployees;
	}

	public void setInactiveEmployees(long inactiveEmployees) {
		this.inactiveEmployees = inactiveEmployees;
	}

}

   

PagedResponse.java

  

package com.sample.app.model;

import java.util.List;

/**
 * Generic pagination wrapper returned by all paginated MCP tools.
 *
 * <p>
 * This is a <b>learning pattern</b>: rather than returning raw lists, we wrap
 * them in a structured response that includes metadata. AI clients can use this
 * metadata to request additional pages or present summary information to users.
 *
 * <p>
 * Example JSON shape:
 * 
 * <pre>
 * {
 *   "currentPage": 0,
 *   "pageSize": 10,
 *   "totalPages": 5,
 *   "totalElements": 47,
 *   "data": [ ... ]
 * }
 * </pre>
 *
 * @param <T> the type of elements in the page (typically {@link Employee})
 */
public class PagedResponse<T> {

	/** Zero-based index of the current page. */
	private int currentPage;

	/** Number of elements requested per page. */
	private int pageSize;

	/** Total number of pages available (= ceil(totalElements / pageSize)). */
	private int totalPages;

	/** Total number of matching elements across all pages. */
	private long totalElements;

	/** The actual elements for the current page. */
	private List<T> data;

	public int getCurrentPage() {
		return currentPage;
	}

	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;
	}

	public int getPageSize() {
		return pageSize;
	}

	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	public int getTotalPages() {
		return totalPages;
	}

	public void setTotalPages(int totalPages) {
		this.totalPages = totalPages;
	}

	public long getTotalElements() {
		return totalElements;
	}

	public void setTotalElements(long totalElements) {
		this.totalElements = totalElements;
	}

	public List<T> getData() {
		return data;
	}

	public void setData(List<T> data) {
		this.data = data;
	}

}

   

CountByGroup.java

 

package com.sample.app.model;

/**
 * Generic key-count pair used by grouping tools.
 *
 * <p>
 * Returned by:
 * <ul>
 * <li>{@code countEmployeesByCountry} — group = country name</li>
 * <li>{@code countEmployeesByDepartment} — group = department name</li>
 * </ul>
 *
 * <p>
 * Example JSON:
 * 
 * <pre>
 * { "group": "India", "count": 28 }
 * </pre>
 */

public class CountByGroup {

	/** The group label (country name, department name, etc.). */
	private String group;

	/** Number of employees in this group. */
	private long count;

	public CountByGroup() {
	}

	public CountByGroup(String group, long count) {
		this.group = group;
		this.count = count;
	}

	public String getGroup() {
		return group;
	}

	public void setGroup(String group) {
		this.group = group;
	}

	public long getCount() {
		return count;
	}

	public void setCount(long count) {
		this.count = count;
	}

}

   

Step 5: Define PaginationUtil class.

 

PaginationUtil.java

package com.sample.app.util;

import java.util.List;

import com.sample.app.model.PagedResponse;

/**
 * Reusable pagination utility for in-memory collections.
 *
 * <p>
 * This utility centralizes all pagination logic in one place, following the
 * <b>DRY (Don't Repeat Yourself)</b> principle. Every paginated tool delegates
 * here.
 *
 * <p>
 * <b>How pagination works:</b>
 * <ol>
 * <li>Validate that pageNumber ≥ 0 and 0 &lt; pageSize ≤ 100</li>
 * <li>Calculate the start index: {@code pageNumber * pageSize}</li>
 * <li>Calculate the end index:
 * {@code min(start + pageSize, totalElements)}</li>
 * <li>Use {@link List#subList} to slice the full list</li>
 * <li>Wrap the slice in a {@link PagedResponse} with metadata</li>
 * </ol>
 */
public final class PaginationUtil {

	/** Maximum allowed page size to protect against accidentally huge responses. */
	private static final int MAX_PAGE_SIZE = 100;

	// Utility class — no instances allowed
	private PaginationUtil() {
		throw new UnsupportedOperationException("Utility class");
	}

	/**
	 * Validates pagination parameters and returns a page from the given list.
	 *
	 * @param <T>        type of list elements
	 * @param allItems   the full (already filtered) list to paginate
	 * @param pageNumber zero-based page index
	 * @param pageSize   number of items per page (1–100)
	 * @return a {@link PagedResponse} with the requested slice and metadata
	 * @throws InvalidPaginationException if pageNumber &lt; 0 or pageSize is out of
	 *                                    range
	 */
	public static <T> PagedResponse<T> paginate(List<T> allItems, int pageNumber, int pageSize) {

		// ── Validation ──────────────────────────────────────────────────────
		if (pageNumber < 0) {
			throw new RuntimeException("pageNumber must be >= 0 (got " + pageNumber + "). Pagination is zero-indexed.");
		}
		if (pageSize <= 0) {
			throw new RuntimeException("pageSize must be > 0 (got " + pageSize + ").");
		}
		if (pageSize > MAX_PAGE_SIZE) {
			throw new RuntimeException("pageSize must be <= " + MAX_PAGE_SIZE + " (got " + pageSize + ").");
		}

		// ── Slice calculation ────────────────────────────────────────────────
		long totalElements = allItems.size();
		int totalPages = (int) Math.ceil((double) totalElements / pageSize);

		// If the requested page is beyond the last page, return an empty page
		// rather than throwing — this allows iteration to stop gracefully.
		if (totalElements == 0 || pageNumber >= totalPages) {
			PagedResponse<T> response = new PagedResponse<>();

			response.setCurrentPage(pageNumber);
			response.setPageSize(pageSize);
			response.setTotalPages(totalPages);
			response.setTotalElements(totalElements);
			response.setData(List.of());

			return response;
		}

		int fromIndex = pageNumber * pageSize;
		int toIndex = (int) Math.min((long) fromIndex + pageSize, totalElements);

		List<T> pageData = allItems.subList(fromIndex, toIndex);

		PagedResponse<T> response = new PagedResponse<>();

		response.setCurrentPage(pageNumber);
		response.setPageSize(pageSize);
		response.setTotalPages(totalPages);
		response.setTotalElements(totalElements);
		response.setData(pageData);

		return response;
	}
}

   

Step 6: Define EmployeeRepository class.

 

EmployeeRepository.java

  

package com.sample.app.repository;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;

import com.sample.app.model.Employee;

/**
 * In-memory employee repository initialized at application startup.
 *
 * <p>
 * This repository replaces a database entirely — no JPA, no SQL, no Hibernate.
 * All 100 employees are created once in the constructor and held in an
 * immutable list.
 *
 * <p>
 * <b>Hierarchy (5 levels):</b>
 * 
 * <pre>
 *   CEO (Aarav Sharma, id=1)
 *     ├── VP Engineering (Priya Mehta, id=2)
 *     │     ├── Director Engineering – Bengaluru (Arjun Sharma, id=5)   → Managers 8,9
 *     │     ├── Director Engineering – Pune (Rohan Kulkarni, id=6)       → Managers 10,11
 *     │     └── Director Engineering – Hyderabad (Siddharth Verma, id=7) → Managers 12,13
 *     ├── VP Finance (Rajesh Patel, id=3)
 *     │     ├── Director Finance (Neha Joshi, id=14)                     → Managers 17,18
 *     │     ├── Director Operations (Priya Nair, id=15)                  → Managers 19,20
 *     │     └── Director Legal (Karthik Menon, id=16)                    → Managers 21,22
 *     └── VP Sales (Ananya Rao, id=4)
 *           ├── Director Sales (Rahul Mehta, id=23)                      → Managers 26,27
 *           ├── Director HR (Ananya Krishnan, id=24)                     → Managers 28,29
 *           └── Director Support (Nikhil Desai, id=25)                   → Managers 30,31
 * </pre>
 */
@Repository
public class EmployeeRepository {

	private static final Logger log = LoggerFactory.getLogger(EmployeeRepository.class);

	/** The complete in-memory dataset. Immutable after construction. */
	private final List<Employee> employees;

	/**
	 * Initializes and populates the in-memory employee list. Logs a summary after
	 * loading.
	 */
	public EmployeeRepository() {
		this.employees = Collections.unmodifiableList(buildEmployees());
		log.info("EmployeeRepository initialized with {} employees across {} levels of hierarchy.", employees.size(),
				5);
	}

	/**
	 * Returns the complete immutable list of all employees.
	 *
	 * @return all employees
	 */
	public List<Employee> findAll() {
		return employees;
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Private data-building helpers
	// ─────────────────────────────────────────────────────────────────────────

	private List<Employee> buildEmployees() {
		List<Employee> list = new ArrayList<>(100);

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 1 — C-Suite
		// ══════════════════════════════════════════════════════════════════════

		list.add(emp(1L, "EMP-0001", "Aarav", "Sharma", "aarav.sharma@acmecorp.com", "+91-9876543210",
				"Chief Executive Officer", "Executive", "Bengaluru", "Karnataka", "India", null, null, 450000.0,
				LocalDate.of(2010, 1, 15), true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 2 — Vice Presidents
		// ══════════════════════════════════════════════════════════════════════
		list.add(emp(2L, "EMP-0002", "Vivaan", "Patel", "vivaan.patel@acmecorp.com", "+91-23456-23456",
				"Vice President", "Engineering", "Hyderabad", "Telangana", "India", 1L, "Aarav Sharma", 280000.0,
				LocalDate.of(2011, 3, 1), true));

		list.add(emp(3L, "EMP-0003", "Aditya", "Reddy", "aditya.reddy@acmecorp.com", "+91-34567-34567",
				"Vice President", "Finance", "Mumbai", "Maharashtra", "India", 1L, "Aarav Sharma", 265000.0,
				LocalDate.of(2012, 6, 15), true));

		list.add(emp(4L, "EMP-0004", "Ananya", "Iyer", "ananya.iyer@acmecorp.com", "+91-45678-45678", "Vice President",
				"Sales", "Chennai", "Tamil Nadu", "India", 1L, "Aarav Sharma", 270000.0, LocalDate.of(2011, 9, 1),
				true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 3 — Directors (9 directors, 3 per VP)
		// ══════════════════════════════════════════════════════════════════════

		// — Under VP Engineering (Vivaan Patel, id=2) —
		list.add(emp(5L, "EMP-0005", "Arjun", "Sharma", "arjun.sharma@acmecorp.com", "+91-56789-56789", "Director",
				"Engineering", "Bengaluru", "Karnataka", "India", 2L, "Vivaan Patel", 195000.0,
				LocalDate.of(2013, 4, 10), true));

		list.add(emp(6L, "EMP-0006", "Rohan", "Kulkarni", "rohan.kulkarni@acmecorp.com", "+91-67890-67890", "Director",
				"Engineering", "Pune", "Maharashtra", "India", 2L, "Vivaan Patel", 190000.0, LocalDate.of(2014, 2, 28),
				true));

		list.add(emp(7L, "EMP-0007", "Siddharth", "Verma", "siddharth.verma@acmecorp.com", "+91-78901-78901",
				"Director", "Engineering", "Hyderabad", "Telangana", "India", 2L, "Vivaan Patel", 185000.0,
				LocalDate.of(2013, 11, 1), true));

		// — Under VP Finance (Aditya Reddy, id=3) —
		list.add(emp(14L, "EMP-0014", "Neha", "Joshi", "neha.joshi@acmecorp.com", "+91-89012-89012", "Director",
				"Finance", "Mumbai", "Maharashtra", "India", 3L, "Aditya Reddy", 185000.0, LocalDate.of(2014, 5, 12),
				true));

		list.add(emp(15L, "EMP-0015", "Priya", "Nair", "priya.nair@acmecorp.com", "+91-90123-90123", "Director",
				"Operations", "Chennai", "Tamil Nadu", "India", 3L, "Aditya Reddy", 178000.0, LocalDate.of(2015, 1, 20),
				true));

		list.add(emp(16L, "EMP-0016", "Karthik", "Menon", "karthik.menon@acmecorp.com", "+91-91234-91234", "Director",
				"Legal", "Kochi", "Kerala", "India", 3L, "Aditya Reddy", 182000.0, LocalDate.of(2014, 8, 5), true));

		// — Under VP Sales (Ananya Iyer, id=4) —
		list.add(emp(23L, "EMP-0023", "Rahul", "Mehta", "rahul.mehta@acmecorp.com", "+91-92345-92345", "Director",
				"Sales", "Ahmedabad", "Gujarat", "India", 4L, "Ananya Iyer", 188000.0, LocalDate.of(2013, 7, 22),
				true));

		list.add(emp(24L, "EMP-0024", "Ananya", "Krishnan", "ananya.krishnan@acmecorp.com", "+91-93456-93456",
				"Director", "HR", "Bengaluru", "Karnataka", "India", 4L, "Ananya Iyer", 175000.0,
				LocalDate.of(2015, 3, 15), true));

		list.add(emp(25L, "EMP-0025", "Nikhil", "Desai", "nikhil.desai@acmecorp.com", "+91-94567-94567", "Director",
				"Support", "Pune", "Maharashtra", "India", 4L, "Ananya Iyer", 180000.0, LocalDate.of(2014, 10, 8),
				true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 4 — Managers (18 managers, 2 per director)
		// ══════════════════════════════════════════════════════════════════════

		// — Under Director Arjun Sharma (id=5, Bengaluru) —
		list.add(emp(8L, "EMP-0008", "Vikram", "Rao", "vikram.rao@acmecorp.com", "+91-95678-95678", "Manager",
				"Engineering", "Bengaluru", "Karnataka", "India", 5L, "Arjun Sharma", 135000.0,
				LocalDate.of(2015, 6, 1), true));

		list.add(emp(9L, "EMP-0009", "Deepa", "Menon", "deepa.menon@acmecorp.com", "+91-96789-96789", "Manager",
				"Engineering", "Hyderabad", "Telangana", "India", 5L, "Arjun Sharma", 130000.0,
				LocalDate.of(2016, 2, 14), true));

		// — Under Director Rohan Kulkarni (id=6, Pune) —
		list.add(emp(10L, "EMP-0010", "Amit", "Joshi", "amit.joshi@acmecorp.com", "+91-97890-97890", "Manager",
				"Engineering", "Pune", "Maharashtra", "India", 6L, "Rohan Kulkarni", 128000.0,
				LocalDate.of(2016, 4, 20), true));

		list.add(emp(11L, "EMP-0011", "Sneha", "Kulkarni", "sneha.kulkarni@acmecorp.com", "+91-98901-98901", "Manager",
				"Engineering", "Nagpur", "Maharashtra", "India", 6L, "Rohan Kulkarni", 125000.0,
				LocalDate.of(2016, 8, 10), true));

		// — Under Director Siddharth Verma (id=7, Hyderabad) —
		list.add(emp(12L, "EMP-0012", "Kiran", "Verma", "kiran.verma@acmecorp.com", "+91-99012-99012", "Manager",
				"Engineering", "Hyderabad", "Telangana", "India", 7L, "Siddharth Verma", 132000.0,
				LocalDate.of(2015, 10, 5), true));

		list.add(emp(13L, "EMP-0013", "Harish", "Gupta", "harish.gupta@acmecorp.com", "+91-90123-01234", "Manager",
				"Engineering", "Noida", "Uttar Pradesh", "India", 7L, "Siddharth Verma", 127000.0,
				LocalDate.of(2017, 1, 9), true));

		// — Under Director Neha Joshi (id=14, Mumbai) —
		list.add(emp(17L, "EMP-0017", "Rakesh", "Gupta", "rakesh.gupta@acmecorp.com", "+91-91234-12345", "Manager",
				"Finance", "Mumbai", "Maharashtra", "India", 14L, "Neha Joshi", 122000.0, LocalDate.of(2016, 3, 7),
				true));

		list.add(emp(18L, "EMP-0018", "Swathi", "Rao", "swathi.rao@acmecorp.com", "+91-92345-23456", "Manager",
				"Finance", "Pune", "Maharashtra", "India", 14L, "Neha Joshi", 118000.0, LocalDate.of(2017, 5, 22),
				true));

		// — Under Director Priya Nair (id=15, Chennai) —
		list.add(emp(19L, "EMP-0019", "Suresh", "Kumar", "suresh.kumar@acmecorp.com", "+91-93456-34567", "Manager",
				"Operations", "Chennai", "Tamil Nadu", "India", 15L, "Priya Nair", 115000.0, LocalDate.of(2016, 11, 30),
				true));

		list.add(emp(20L, "EMP-0020", "Pooja", "Singh", "pooja.singh@acmecorp.com", "+91-94567-45678", "Manager",
				"Operations", "New Delhi", "Delhi", "India", 15L, "Priya Nair", 112000.0, LocalDate.of(2017, 7, 18),
				true));

		// — Under Director Karthik Menon (id=16, Kochi) —
		list.add(emp(21L, "EMP-0021", "Meera", "Nambiar", "meera.nambiar@acmecorp.com", "+91-95678-56789", "Manager",
				"Legal", "Kochi", "Kerala", "India", 16L, "Karthik Menon", 120000.0, LocalDate.of(2016, 9, 12), true));

		list.add(emp(22L, "EMP-0022", "Nitin", "Bhat", "nitin.bhat@acmecorp.com", "+91-96789-67890", "Manager", "Legal",
				"Mangaluru", "Karnataka", "India", 16L, "Karthik Menon", 118000.0, LocalDate.of(2017, 2, 28), true));

		// — Under Director Rahul Mehta (id=23, Ahmedabad) —
		list.add(emp(26L, "EMP-0026", "Tarun", "Shah", "tarun.shah@acmecorp.com", "+91-97890-78901", "Manager", "Sales",
				"Ahmedabad", "Gujarat", "India", 23L, "Rahul Mehta", 115000.0, LocalDate.of(2016, 6, 5), true));

		list.add(emp(27L, "EMP-0027", "Bhavna", "Patel", "bhavna.patel@acmecorp.com", "+91-98901-89012", "Manager",
				"Sales", "Surat", "Gujarat", "India", 23L, "Rahul Mehta", 112000.0, LocalDate.of(2017, 4, 14), true));

		// — Under Director Ananya Krishnan (id=24, Bengaluru) —
		list.add(emp(28L, "EMP-0028", "Kavya", "Reddy", "kavya.reddy@acmecorp.com", "+91-99012-90123", "Manager", "HR",
				"Bengaluru", "Karnataka", "India", 24L, "Ananya Krishnan", 108000.0, LocalDate.of(2017, 8, 1), true));

		list.add(emp(29L, "EMP-0029", "Ravi", "Iyer", "ravi.iyer@acmecorp.com", "+91-90123-91234", "Manager",
				"Marketing", "Hyderabad", "Telangana", "India", 24L, "Ananya Krishnan", 110000.0,
				LocalDate.of(2018, 1, 15), true));

		// — Under Director Nikhil Desai (id=25, Pune) —
		list.add(emp(30L, "EMP-0030", "Aishwarya", "Kulkarni", "aishwarya.kulkarni@acmecorp.com", "+91-91234-92345",
				"Manager", "Support", "Pune", "Maharashtra", "India", 25L, "Nikhil Desai", 112000.0,
				LocalDate.of(2017, 11, 20), true));

		list.add(emp(31L, "EMP-0031", "Prashant", "Jain", "prashant.jain@acmecorp.com", "+91-92345-93456", "Manager",
				"Security", "Jaipur", "Rajasthan", "India", 25L, "Nikhil Desai", 115000.0, LocalDate.of(2018, 3, 10),
				true));

		// — Under Manager Vikram Rao (id=8) — Engineering, Bengaluru, India —
		list.add(emp(32L, "EMP-0032", "Aditya", "Verma", "aditya.verma@acmecorp.com", "+91-90111-10001",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 78000.0,
				LocalDate.of(2019, 7, 1), true));

		list.add(emp(33L, "EMP-0033", "Sneha", "Joshi", "sneha.joshi@acmecorp.com", "+91-90111-10002",
				"Senior Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 95000.0,
				LocalDate.of(2018, 4, 15), true));

		list.add(emp(34L, "EMP-0034", "Karan", "Malhotra", "karan.malhotra@acmecorp.com", "+91-90111-10003",
				"Lead Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 108000.0,
				LocalDate.of(2017, 6, 20), true));

		list.add(emp(35L, "EMP-0035", "Ishaan", "Gupta", "ishaan.gupta@acmecorp.com", "+91-90111-10004",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 76000.0,
				LocalDate.of(2020, 3, 2), true));

		// — Under Manager Deepa Menon (id=9) — Engineering, Hyderabad, India —
		list.add(emp(36L, "EMP-0036", "Tanmay", "Kulkarni", "tanmay.kulkarni@acmecorp.com", "+91-90111-10005",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 80000.0,
				LocalDate.of(2019, 9, 16), true));

		list.add(emp(37L, "EMP-0037", "Lakshmi", "Prasad", "lakshmi.prasad@acmecorp.com", "+91-90111-10006",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 97000.0,
				LocalDate.of(2018, 7, 30), true));

		list.add(emp(38L, "EMP-0038", "Nikhil", "Deshpande", "nikhil.deshpande@acmecorp.com", "+91-90111-10007",
				"Architect", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 115000.0,
				LocalDate.of(2017, 2, 1), true));

		list.add(emp(39L, "EMP-0039", "Ritika", "Saxena", "ritika.saxena@acmecorp.com", "+91-90111-10008",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 79000.0,
				LocalDate.of(2020, 1, 6), true));

		// — Under Manager Amit Joshi (id=10) — Engineering, Pune, India —
		list.add(emp(40L, "EMP-0040", "Ananya", "Kulkarni", "ananya.kulkarni@acmecorp.com", "+91-90111-10009",
				"Senior Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 98000.0,
				LocalDate.of(2018, 11, 12), true));

		list.add(emp(41L, "EMP-0041", "Rohit", "Patil", "rohit.patil@acmecorp.com", "+91-90111-10010", "Lead Engineer",
				"Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 110000.0, LocalDate.of(2017, 9, 5),
				true));

		list.add(emp(42L, "EMP-0042", "Pooja", "Desai", "pooja.desai@acmecorp.com", "+91-90111-10011",
				"Software Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 82000.0,
				LocalDate.of(2019, 5, 27), true));

		list.add(emp(43L, "EMP-0043", "Sandeep", "Kulkarni", "sandeep.kulkarni@acmecorp.com", "+91-90111-10012",
				"Architect", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 118000.0,
				LocalDate.of(2016, 12, 19), true));

		// — Under Manager Sneha Kulkarni (id=11) — Engineering, Nagpur, India —
		list.add(emp(44L, "EMP-0044", "Chaitra", "Patil", "chaitra.patil@acmecorp.com", "+91-90111-10013",
				"Senior Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 96000.0,
				LocalDate.of(2018, 8, 3), true));

		list.add(emp(45L, "EMP-0045", "Harsh", "Mehta", "harsh.mehta@acmecorp.com", "+91-90111-10014",
				"Software Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 82000.0,
				LocalDate.of(2019, 4, 22), true));

		list.add(emp(46L, "EMP-0046", "Megha", "Sharma", "megha.sharma@acmecorp.com", "+91-90111-10015",
				"Lead Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 109000.0,
				LocalDate.of(2017, 11, 14), true));

		list.add(emp(47L, "EMP-0047", "Nitin", "Rao", "nitin.rao@acmecorp.com", "+91-90111-10016", "Software Engineer",
				"Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 80000.0,
				LocalDate.of(2020, 6, 1), false));

		// — Under Manager Kiran Verma (id=12) — Engineering, Hyderabad, India —
		list.add(emp(48L, "EMP-0048", "Harini", "Reddy", "harini.reddy@acmecorp.com", "+91-90111-10017",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 94000.0,
				LocalDate.of(2018, 10, 8), true));

		list.add(emp(49L, "EMP-0049", "Rakesh", "Naidu", "rakesh.naidu@acmecorp.com", "+91-90111-10018",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 81000.0,
				LocalDate.of(2019, 12, 2), true));

		list.add(emp(50L, "EMP-0050", "Sowmya", "Iyer", "sowmya.iyer@acmecorp.com", "+91-90111-10019", "Architect",
				"Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 116000.0,
				LocalDate.of(2017, 3, 25), true));

		// — Under Manager Harish Gupta (id=13) — Engineering, Noida, India —
		list.add(emp(51L, "EMP-0051", "Daivik", "Sharma", "daivik.sharma@acmecorp.com", "+91-90111-10020",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 79000.0,
				LocalDate.of(2020, 4, 13), true));

		list.add(emp(52L, "EMP-0052", "Aakash", "Agarwal", "aakash.agarwal@acmecorp.com", "+91-90111-10021",
				"Senior Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 92000.0,
				LocalDate.of(2019, 2, 18), true));

		list.add(emp(53L, "EMP-0053", "Rohit", "Bansal", "rohit.bansal@acmecorp.com", "+91-90111-10022",
				"Lead Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 107000.0,
				LocalDate.of(2018, 6, 25), true));

		list.add(emp(54L, "EMP-0054", "Pallavi", "Mishra", "pallavi.mishra@acmecorp.com", "+91-90111-10023",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 85000.0,
				LocalDate.of(2019, 10, 7), true));

		list.add(emp(55L, "EMP-0055", "Vivek", "Srivastava", "vivek.srivastava@acmecorp.com", "+91-90111-10024",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 83000.0,
				LocalDate.of(2020, 8, 3), false));

		// — Under Manager Rakesh Gupta (id=17) — Finance, Mumbai, India —
		list.add(emp(56L, "EMP-0056", "Kavita", "Shah", "kavita.shah@acmecorp.com", "+91-90111-10025",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 72000.0,
				LocalDate.of(2019, 3, 11), true));

		list.add(emp(57L, "EMP-0057", "Rahul", "Kulkarni", "rahul.kulkarni@acmecorp.com", "+91-90111-10026",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 75000.0,
				LocalDate.of(2018, 9, 24), true));

		list.add(emp(58L, "EMP-0058", "Nisha", "Patel", "nisha.patel@acmecorp.com", "+91-90111-10027",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 73000.0,
				LocalDate.of(2020, 2, 17), true));

		// — Under Manager Swathi Rao (id=18) — Finance, Pune, India —
		list.add(emp(59L, "EMP-0059", "Manoj", "Patil", "manoj.patil@acmecorp.com", "+91-90111-10028",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 71000.0,
				LocalDate.of(2019, 6, 3), true));

		list.add(emp(60L, "EMP-0060", "Aarti", "Kulkarni", "aarti.kulkarni@acmecorp.com", "+91-90111-10029",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 74000.0,
				LocalDate.of(2018, 12, 10), true));

		list.add(emp(61L, "EMP-0061", "Naveen", "Joshi", "naveen.joshi@acmecorp.com", "+91-90111-10030",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 70000.0,
				LocalDate.of(2020, 5, 19), false));

		// — Under Manager Suresh Kumar (id=19) — Operations, Chennai, India —
		list.add(emp(62L, "EMP-0062", "Meera", "Pillai", "meera.pillai@acmecorp.com", "+91-90111-10031",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 65000.0,
				LocalDate.of(2019, 8, 5), true));

		list.add(emp(63L, "EMP-0063", "Sanjay", "Nair", "sanjay.nair@acmecorp.com", "+91-90111-10032",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 67000.0,
				LocalDate.of(2018, 11, 19), true));

		list.add(emp(64L, "EMP-0064", "Divya", "Krishnamurthy", "divya.krishnamurthy@acmecorp.com", "+91-90111-10033",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 66000.0,
				LocalDate.of(2020, 1, 27), true));

		// — Under Manager Pooja Singh (id=20) — Operations, New Delhi, India —
		list.add(emp(65L, "EMP-0065", "Amit", "Tiwari", "amit.tiwari@acmecorp.com", "+91-90111-10034",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 64000.0,
				LocalDate.of(2019, 11, 4), true));

		list.add(emp(66L, "EMP-0066", "Neha", "Sharma", "neha.sharma@acmecorp.com", "+91-90111-10035",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 65000.0,
				LocalDate.of(2018, 5, 8), true));

		list.add(emp(67L, "EMP-0067", "Rohit", "Chauhan", "rohit.chauhan@acmecorp.com", "+91-90111-10036",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 63000.0,
				LocalDate.of(2020, 9, 14), false));

		// — Under Manager Meera Nambiar (id=21) — Legal, Kochi, India —
		list.add(emp(68L, "EMP-0068", "Anand", "Menon", "anand.menon@acmecorp.com", "+91-90111-10037", "Legal Analyst",
				"Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 78000.0, LocalDate.of(2019, 4, 16), true));

		list.add(emp(69L, "EMP-0069", "Keerthi", "Nair", "keerthi.nair@acmecorp.com", "+91-90111-10038",
				"Legal Analyst", "Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 76000.0,
				LocalDate.of(2018, 10, 3), true));

		list.add(emp(70L, "EMP-0070", "Arvind", "Bhat", "arvind.bhat@acmecorp.com", "+91-90111-10039", "Legal Analyst",
				"Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 79000.0, LocalDate.of(2020, 7, 20), true));

		// — Under Manager Nitin Bhat (id=22) — Legal, Mangaluru, India —
		list.add(emp(71L, "EMP-0071", "Anjali", "Shetty", "anjali.shetty@acmecorp.com", "+91-90111-10040",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 77000.0,
				LocalDate.of(2019, 1, 7), true));

		list.add(emp(72L, "EMP-0072", "Karthik", "Hegde", "karthik.hegde@acmecorp.com", "+91-90111-10041",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 75000.0,
				LocalDate.of(2018, 6, 14), true));

		list.add(emp(73L, "EMP-0073", "Pavithra", "Rao", "pavithra.rao@acmecorp.com", "+91-90111-10042",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 76000.0,
				LocalDate.of(2020, 3, 25), false));

		// — Under Manager Tarun Shah (id=26) — Sales, Ahmedabad, India —
		list.add(emp(74L, "EMP-0074", "Ritesh", "Patel", "ritesh.patel@acmecorp.com", "+91-90111-10043",
				"Sales Executive", "Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 68000.0,
				LocalDate.of(2019, 9, 9), true));

		list.add(emp(75L, "EMP-0075", "Neha", "Desai", "neha.desai@acmecorp.com", "+91-90111-10044", "Sales Executive",
				"Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 70000.0, LocalDate.of(2018, 4, 2), true));

		list.add(emp(76L, "EMP-0076", "Jay", "Mehta", "jay.mehta@acmecorp.com", "+91-90111-10045", "Sales Executive",
				"Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 67000.0, LocalDate.of(2020, 11, 10),
				true));

		// — Under Manager Bhavna Patel (id=27) — Sales, Surat, India —
		list.add(emp(77L, "EMP-0077", "Dhruv", "Shah", "dhruv.shah@acmecorp.com", "+91-90111-10046", "Sales Executive",
				"Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 69000.0, LocalDate.of(2019, 7, 21), true));

		list.add(emp(78L, "EMP-0078", "Riya", "Joshi", "riya.joshi@acmecorp.com", "+91-90111-10047", "Sales Executive",
				"Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 71000.0, LocalDate.of(2018, 2, 6), true));

		list.add(emp(79L, "EMP-0079", "Yash", "Trivedi", "yash.trivedi@acmecorp.com", "+91-90111-10048",
				"Sales Executive", "Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 68000.0,
				LocalDate.of(2020, 10, 12), false));

		// — Under Manager Kavya Reddy (id=28) — HR, Bengaluru, India —
		list.add(emp(80L, "EMP-0080", "Shruti", "Bhat", "shruti.bhat@acmecorp.com", "+91-90111-10049", "HR Executive",
				"HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 62000.0, LocalDate.of(2019, 5, 30), true));

		list.add(emp(81L, "EMP-0081", "Tejas", "Kulkarni", "tejas.kulkarni@acmecorp.com", "+91-90111-10050",
				"HR Executive", "HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 63000.0,
				LocalDate.of(2018, 9, 17), true));

		list.add(emp(82L, "EMP-0082", "Pallavi", "Gaikwad", "pallavi.gaikwad@acmecorp.com", "+91-90111-10051",
				"HR Executive", "HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 61000.0,
				LocalDate.of(2020, 4, 6), true));

		// — Under Manager Ravi Iyer (id=29) — Marketing, Hyderabad, India —
		list.add(emp(83L, "EMP-0083", "Chitra", "Narayanan", "chitra.narayanan@acmecorp.com", "+91-90111-10052",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 65000.0,
				LocalDate.of(2019, 3, 24), true));

		list.add(emp(84L, "EMP-0084", "Vishal", "Kapoor", "vishal.kapoor@acmecorp.com", "+91-90111-10053",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 66000.0,
				LocalDate.of(2018, 8, 11), true));

		list.add(emp(85L, "EMP-0085", "Priyanka", "Rao", "priyanka.rao@acmecorp.com", "+91-90111-10054",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 64000.0,
				LocalDate.of(2020, 6, 8), true));

		// — Under Manager Aishwarya Kulkarni (id=30) — Support, Pune, India —
		list.add(emp(86L, "EMP-0086", "Rahul", "Jadhav", "rahul.jadhav@acmecorp.com", "+91-90111-10055",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 67000.0,
				LocalDate.of(2019, 2, 14), true));

		list.add(emp(87L, "EMP-0087", "Snehal", "Patil", "snehal.patil@acmecorp.com", "+91-90111-10056",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 68000.0,
				LocalDate.of(2018, 7, 28), true));

		list.add(emp(88L, "EMP-0088", "Mahesh", "Pawar", "mahesh.pawar@acmecorp.com", "+91-90111-10057",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 65000.0,
				LocalDate.of(2020, 1, 20), true));

		list.add(emp(89L, "EMP-0089", "Aniket", "Shinde", "aniket.shinde@acmecorp.com", "+91-90111-10058",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 64000.0,
				LocalDate.of(2021, 3, 1), true));

		// — Under Manager Prashant Jain (id=31) — Security, Jaipur, India —
		list.add(emp(90L, "EMP-0090", "Gaurav", "Mathur", "gaurav.mathur@acmecorp.com", "+91-90111-10059",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 88000.0,
				LocalDate.of(2019, 6, 10), true));

		list.add(emp(91L, "EMP-0091", "Nidhi", "Sharma", "nidhi.sharma@acmecorp.com", "+91-90111-10060",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 87000.0,
				LocalDate.of(2018, 11, 5), true));

		list.add(emp(92L, "EMP-0092", "Saurabh", "Agarwal", "saurabh.agarwal@acmecorp.com", "+91-90111-10061",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 86000.0,
				LocalDate.of(2020, 8, 17), false));

		// ── Additional employees to reach 100 ─────────────────────────────────

		// More Engineers under Vikram Rao (id=8)
		list.add(emp(93L, "EMP-0093", "Rahul", "Mehta", "rahul.mehta@acmecorp.com", "+91-90111-10062",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 77000.0,
				LocalDate.of(2021, 2, 15), true));

		list.add(emp(94L, "EMP-0094", "Poornima", "Rajan", "poornima.rajan@acmecorp.com", "+91-90111-10063",
				"Senior Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 96000.0,
				LocalDate.of(2020, 10, 19), true));

		// More Engineers under Deepa Menon (id=9)
		list.add(emp(95L, "EMP-0095", "Varun", "Bhatt", "varun.bhatt@acmecorp.com", "+91-90111-10064",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 78000.0,
				LocalDate.of(2021, 5, 3), true));

		list.add(emp(96L, "EMP-0096", "Shweta", "Pandey", "shweta.pandey@acmecorp.com", "+91-90111-10065",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 94000.0,
				LocalDate.of(2020, 12, 7), true));

		// More Engineers under Amit Joshi (id=10)
		list.add(emp(97L, "EMP-0097", "Sandhya", "Kulkarni", "sandhya.kulkarni@acmecorp.com", "+91-90111-10066",
				"Software Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 83000.0,
				LocalDate.of(2021, 1, 11), true));

		list.add(emp(98L, "EMP-0098", "Pradeep", "Jain", "pradeep.jain@acmecorp.com", "+91-90111-10067",
				"Senior Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 99000.0,
				LocalDate.of(2020, 7, 27), true));

		// More Support Engineers under Aishwarya Kulkarni (id=30)
		list.add(emp(99L, "EMP-0099", "Yogesh", "Patil", "yogesh.patil@acmecorp.com", "+91-90111-10068",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 66000.0,
				LocalDate.of(2021, 4, 19), true));

		list.add(emp(100L, "EMP-0100", "Tina", "Joshi", "tina.joshi@acmecorp.com", "+91-90111-10069",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 65000.0,
				LocalDate.of(2021, 9, 6), true));

		return list;
	}

	/**
	 * Factory method to build an {@link Employee} instance from individual fields.
	 * Computes {@code fullName} automatically from first and last name.
	 */
	private static Employee emp(Long id, String empNum, String firstName, String lastName, String email, String phone,
			String designation, String department, String city, String state, String country, Long managerId,
			String managerName, Double salary, LocalDate joiningDate, boolean active) {

		Employee emp = new Employee();

		emp.setId(id);
		emp.setEmployeeNumber(empNum);
		emp.setFirstName(firstName);
		emp.setLastName(lastName);
		emp.setFullName(firstName + " " + lastName);
		emp.setEmail(email);
		emp.setPhone(phone);
		emp.setDesignation(designation);
		emp.setDepartment(department);
		emp.setCity(city);
		emp.setState(state);
		emp.setCountry(country);
		emp.setManagerId(managerId);
		emp.setManagerName(managerName);
		emp.setSalary(salary);
		emp.setJoiningDate(joiningDate);
		emp.setActive(active);

		return emp;
	}
}

   

Step 7: Define EmployeeService class.

 

EmployeeService.java

package com.sample.app.repository;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;

import com.sample.app.model.Employee;

/**
 * In-memory employee repository initialized at application startup.
 *
 * <p>
 * This repository replaces a database entirely — no JPA, no SQL, no Hibernate.
 * All 100 employees are created once in the constructor and held in an
 * immutable list.
 *
 * <p>
 * <b>Hierarchy (5 levels):</b>
 * 
 * <pre>
 *   CEO (Aarav Sharma, id=1)
 *     ├── VP Engineering (Priya Mehta, id=2)
 *     │     ├── Director Engineering – Bengaluru (Arjun Sharma, id=5)   → Managers 8,9
 *     │     ├── Director Engineering – Pune (Rohan Kulkarni, id=6)       → Managers 10,11
 *     │     └── Director Engineering – Hyderabad (Siddharth Verma, id=7) → Managers 12,13
 *     ├── VP Finance (Rajesh Patel, id=3)
 *     │     ├── Director Finance (Neha Joshi, id=14)                     → Managers 17,18
 *     │     ├── Director Operations (Priya Nair, id=15)                  → Managers 19,20
 *     │     └── Director Legal (Karthik Menon, id=16)                    → Managers 21,22
 *     └── VP Sales (Ananya Rao, id=4)
 *           ├── Director Sales (Rahul Mehta, id=23)                      → Managers 26,27
 *           ├── Director HR (Ananya Krishnan, id=24)                     → Managers 28,29
 *           └── Director Support (Nikhil Desai, id=25)                   → Managers 30,31
 * </pre>
 */
@Repository
public class EmployeeRepository {

	private static final Logger log = LoggerFactory.getLogger(EmployeeRepository.class);

	/** The complete in-memory dataset. Immutable after construction. */
	private final List<Employee> employees;

	/**
	 * Initializes and populates the in-memory employee list. Logs a summary after
	 * loading.
	 */
	public EmployeeRepository() {
		this.employees = Collections.unmodifiableList(buildEmployees());
		log.info("EmployeeRepository initialized with {} employees across {} levels of hierarchy.", employees.size(),
				5);
	}

	/**
	 * Returns the complete immutable list of all employees.
	 *
	 * @return all employees
	 */
	public List<Employee> findAll() {
		return employees;
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Private data-building helpers
	// ─────────────────────────────────────────────────────────────────────────

	private List<Employee> buildEmployees() {
		List<Employee> list = new ArrayList<>(100);

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 1 — C-Suite
		// ══════════════════════════════════════════════════════════════════════

		list.add(emp(1L, "EMP-0001", "Aarav", "Sharma", "aarav.sharma@acmecorp.com", "+91-9876543210",
				"Chief Executive Officer", "Executive", "Bengaluru", "Karnataka", "India", null, null, 450000.0,
				LocalDate.of(2010, 1, 15), true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 2 — Vice Presidents
		// ══════════════════════════════════════════════════════════════════════
		list.add(emp(2L, "EMP-0002", "Vivaan", "Patel", "vivaan.patel@acmecorp.com", "+91-23456-23456",
				"Vice President", "Engineering", "Hyderabad", "Telangana", "India", 1L, "Aarav Sharma", 280000.0,
				LocalDate.of(2011, 3, 1), true));

		list.add(emp(3L, "EMP-0003", "Aditya", "Reddy", "aditya.reddy@acmecorp.com", "+91-34567-34567",
				"Vice President", "Finance", "Mumbai", "Maharashtra", "India", 1L, "Aarav Sharma", 265000.0,
				LocalDate.of(2012, 6, 15), true));

		list.add(emp(4L, "EMP-0004", "Ananya", "Iyer", "ananya.iyer@acmecorp.com", "+91-45678-45678", "Vice President",
				"Sales", "Chennai", "Tamil Nadu", "India", 1L, "Aarav Sharma", 270000.0, LocalDate.of(2011, 9, 1),
				true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 3 — Directors (9 directors, 3 per VP)
		// ══════════════════════════════════════════════════════════════════════

		// — Under VP Engineering (Vivaan Patel, id=2) —
		list.add(emp(5L, "EMP-0005", "Arjun", "Sharma", "arjun.sharma@acmecorp.com", "+91-56789-56789", "Director",
				"Engineering", "Bengaluru", "Karnataka", "India", 2L, "Vivaan Patel", 195000.0,
				LocalDate.of(2013, 4, 10), true));

		list.add(emp(6L, "EMP-0006", "Rohan", "Kulkarni", "rohan.kulkarni@acmecorp.com", "+91-67890-67890", "Director",
				"Engineering", "Pune", "Maharashtra", "India", 2L, "Vivaan Patel", 190000.0, LocalDate.of(2014, 2, 28),
				true));

		list.add(emp(7L, "EMP-0007", "Siddharth", "Verma", "siddharth.verma@acmecorp.com", "+91-78901-78901",
				"Director", "Engineering", "Hyderabad", "Telangana", "India", 2L, "Vivaan Patel", 185000.0,
				LocalDate.of(2013, 11, 1), true));

		// — Under VP Finance (Aditya Reddy, id=3) —
		list.add(emp(14L, "EMP-0014", "Neha", "Joshi", "neha.joshi@acmecorp.com", "+91-89012-89012", "Director",
				"Finance", "Mumbai", "Maharashtra", "India", 3L, "Aditya Reddy", 185000.0, LocalDate.of(2014, 5, 12),
				true));

		list.add(emp(15L, "EMP-0015", "Priya", "Nair", "priya.nair@acmecorp.com", "+91-90123-90123", "Director",
				"Operations", "Chennai", "Tamil Nadu", "India", 3L, "Aditya Reddy", 178000.0, LocalDate.of(2015, 1, 20),
				true));

		list.add(emp(16L, "EMP-0016", "Karthik", "Menon", "karthik.menon@acmecorp.com", "+91-91234-91234", "Director",
				"Legal", "Kochi", "Kerala", "India", 3L, "Aditya Reddy", 182000.0, LocalDate.of(2014, 8, 5), true));

		// — Under VP Sales (Ananya Iyer, id=4) —
		list.add(emp(23L, "EMP-0023", "Rahul", "Mehta", "rahul.mehta@acmecorp.com", "+91-92345-92345", "Director",
				"Sales", "Ahmedabad", "Gujarat", "India", 4L, "Ananya Iyer", 188000.0, LocalDate.of(2013, 7, 22),
				true));

		list.add(emp(24L, "EMP-0024", "Ananya", "Krishnan", "ananya.krishnan@acmecorp.com", "+91-93456-93456",
				"Director", "HR", "Bengaluru", "Karnataka", "India", 4L, "Ananya Iyer", 175000.0,
				LocalDate.of(2015, 3, 15), true));

		list.add(emp(25L, "EMP-0025", "Nikhil", "Desai", "nikhil.desai@acmecorp.com", "+91-94567-94567", "Director",
				"Support", "Pune", "Maharashtra", "India", 4L, "Ananya Iyer", 180000.0, LocalDate.of(2014, 10, 8),
				true));

		// ══════════════════════════════════════════════════════════════════════
		// LEVEL 4 — Managers (18 managers, 2 per director)
		// ══════════════════════════════════════════════════════════════════════

		// — Under Director Arjun Sharma (id=5, Bengaluru) —
		list.add(emp(8L, "EMP-0008", "Vikram", "Rao", "vikram.rao@acmecorp.com", "+91-95678-95678", "Manager",
				"Engineering", "Bengaluru", "Karnataka", "India", 5L, "Arjun Sharma", 135000.0,
				LocalDate.of(2015, 6, 1), true));

		list.add(emp(9L, "EMP-0009", "Deepa", "Menon", "deepa.menon@acmecorp.com", "+91-96789-96789", "Manager",
				"Engineering", "Hyderabad", "Telangana", "India", 5L, "Arjun Sharma", 130000.0,
				LocalDate.of(2016, 2, 14), true));

		// — Under Director Rohan Kulkarni (id=6, Pune) —
		list.add(emp(10L, "EMP-0010", "Amit", "Joshi", "amit.joshi@acmecorp.com", "+91-97890-97890", "Manager",
				"Engineering", "Pune", "Maharashtra", "India", 6L, "Rohan Kulkarni", 128000.0,
				LocalDate.of(2016, 4, 20), true));

		list.add(emp(11L, "EMP-0011", "Sneha", "Kulkarni", "sneha.kulkarni@acmecorp.com", "+91-98901-98901", "Manager",
				"Engineering", "Nagpur", "Maharashtra", "India", 6L, "Rohan Kulkarni", 125000.0,
				LocalDate.of(2016, 8, 10), true));

		// — Under Director Siddharth Verma (id=7, Hyderabad) —
		list.add(emp(12L, "EMP-0012", "Kiran", "Verma", "kiran.verma@acmecorp.com", "+91-99012-99012", "Manager",
				"Engineering", "Hyderabad", "Telangana", "India", 7L, "Siddharth Verma", 132000.0,
				LocalDate.of(2015, 10, 5), true));

		list.add(emp(13L, "EMP-0013", "Harish", "Gupta", "harish.gupta@acmecorp.com", "+91-90123-01234", "Manager",
				"Engineering", "Noida", "Uttar Pradesh", "India", 7L, "Siddharth Verma", 127000.0,
				LocalDate.of(2017, 1, 9), true));

		// — Under Director Neha Joshi (id=14, Mumbai) —
		list.add(emp(17L, "EMP-0017", "Rakesh", "Gupta", "rakesh.gupta@acmecorp.com", "+91-91234-12345", "Manager",
				"Finance", "Mumbai", "Maharashtra", "India", 14L, "Neha Joshi", 122000.0, LocalDate.of(2016, 3, 7),
				true));

		list.add(emp(18L, "EMP-0018", "Swathi", "Rao", "swathi.rao@acmecorp.com", "+91-92345-23456", "Manager",
				"Finance", "Pune", "Maharashtra", "India", 14L, "Neha Joshi", 118000.0, LocalDate.of(2017, 5, 22),
				true));

		// — Under Director Priya Nair (id=15, Chennai) —
		list.add(emp(19L, "EMP-0019", "Suresh", "Kumar", "suresh.kumar@acmecorp.com", "+91-93456-34567", "Manager",
				"Operations", "Chennai", "Tamil Nadu", "India", 15L, "Priya Nair", 115000.0, LocalDate.of(2016, 11, 30),
				true));

		list.add(emp(20L, "EMP-0020", "Pooja", "Singh", "pooja.singh@acmecorp.com", "+91-94567-45678", "Manager",
				"Operations", "New Delhi", "Delhi", "India", 15L, "Priya Nair", 112000.0, LocalDate.of(2017, 7, 18),
				true));

		// — Under Director Karthik Menon (id=16, Kochi) —
		list.add(emp(21L, "EMP-0021", "Meera", "Nambiar", "meera.nambiar@acmecorp.com", "+91-95678-56789", "Manager",
				"Legal", "Kochi", "Kerala", "India", 16L, "Karthik Menon", 120000.0, LocalDate.of(2016, 9, 12), true));

		list.add(emp(22L, "EMP-0022", "Nitin", "Bhat", "nitin.bhat@acmecorp.com", "+91-96789-67890", "Manager", "Legal",
				"Mangaluru", "Karnataka", "India", 16L, "Karthik Menon", 118000.0, LocalDate.of(2017, 2, 28), true));

		// — Under Director Rahul Mehta (id=23, Ahmedabad) —
		list.add(emp(26L, "EMP-0026", "Tarun", "Shah", "tarun.shah@acmecorp.com", "+91-97890-78901", "Manager", "Sales",
				"Ahmedabad", "Gujarat", "India", 23L, "Rahul Mehta", 115000.0, LocalDate.of(2016, 6, 5), true));

		list.add(emp(27L, "EMP-0027", "Bhavna", "Patel", "bhavna.patel@acmecorp.com", "+91-98901-89012", "Manager",
				"Sales", "Surat", "Gujarat", "India", 23L, "Rahul Mehta", 112000.0, LocalDate.of(2017, 4, 14), true));

		// — Under Director Ananya Krishnan (id=24, Bengaluru) —
		list.add(emp(28L, "EMP-0028", "Kavya", "Reddy", "kavya.reddy@acmecorp.com", "+91-99012-90123", "Manager", "HR",
				"Bengaluru", "Karnataka", "India", 24L, "Ananya Krishnan", 108000.0, LocalDate.of(2017, 8, 1), true));

		list.add(emp(29L, "EMP-0029", "Ravi", "Iyer", "ravi.iyer@acmecorp.com", "+91-90123-91234", "Manager",
				"Marketing", "Hyderabad", "Telangana", "India", 24L, "Ananya Krishnan", 110000.0,
				LocalDate.of(2018, 1, 15), true));

		// — Under Director Nikhil Desai (id=25, Pune) —
		list.add(emp(30L, "EMP-0030", "Aishwarya", "Kulkarni", "aishwarya.kulkarni@acmecorp.com", "+91-91234-92345",
				"Manager", "Support", "Pune", "Maharashtra", "India", 25L, "Nikhil Desai", 112000.0,
				LocalDate.of(2017, 11, 20), true));

		list.add(emp(31L, "EMP-0031", "Prashant", "Jain", "prashant.jain@acmecorp.com", "+91-92345-93456", "Manager",
				"Security", "Jaipur", "Rajasthan", "India", 25L, "Nikhil Desai", 115000.0, LocalDate.of(2018, 3, 10),
				true));

		// — Under Manager Vikram Rao (id=8) — Engineering, Bengaluru, India —
		list.add(emp(32L, "EMP-0032", "Aditya", "Verma", "aditya.verma@acmecorp.com", "+91-90111-10001",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 78000.0,
				LocalDate.of(2019, 7, 1), true));

		list.add(emp(33L, "EMP-0033", "Sneha", "Joshi", "sneha.joshi@acmecorp.com", "+91-90111-10002",
				"Senior Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 95000.0,
				LocalDate.of(2018, 4, 15), true));

		list.add(emp(34L, "EMP-0034", "Karan", "Malhotra", "karan.malhotra@acmecorp.com", "+91-90111-10003",
				"Lead Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 108000.0,
				LocalDate.of(2017, 6, 20), true));

		list.add(emp(35L, "EMP-0035", "Ishaan", "Gupta", "ishaan.gupta@acmecorp.com", "+91-90111-10004",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 76000.0,
				LocalDate.of(2020, 3, 2), true));

		// — Under Manager Deepa Menon (id=9) — Engineering, Hyderabad, India —
		list.add(emp(36L, "EMP-0036", "Tanmay", "Kulkarni", "tanmay.kulkarni@acmecorp.com", "+91-90111-10005",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 80000.0,
				LocalDate.of(2019, 9, 16), true));

		list.add(emp(37L, "EMP-0037", "Lakshmi", "Prasad", "lakshmi.prasad@acmecorp.com", "+91-90111-10006",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 97000.0,
				LocalDate.of(2018, 7, 30), true));

		list.add(emp(38L, "EMP-0038", "Nikhil", "Deshpande", "nikhil.deshpande@acmecorp.com", "+91-90111-10007",
				"Architect", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 115000.0,
				LocalDate.of(2017, 2, 1), true));

		list.add(emp(39L, "EMP-0039", "Ritika", "Saxena", "ritika.saxena@acmecorp.com", "+91-90111-10008",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 79000.0,
				LocalDate.of(2020, 1, 6), true));

		// — Under Manager Amit Joshi (id=10) — Engineering, Pune, India —
		list.add(emp(40L, "EMP-0040", "Ananya", "Kulkarni", "ananya.kulkarni@acmecorp.com", "+91-90111-10009",
				"Senior Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 98000.0,
				LocalDate.of(2018, 11, 12), true));

		list.add(emp(41L, "EMP-0041", "Rohit", "Patil", "rohit.patil@acmecorp.com", "+91-90111-10010", "Lead Engineer",
				"Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 110000.0, LocalDate.of(2017, 9, 5),
				true));

		list.add(emp(42L, "EMP-0042", "Pooja", "Desai", "pooja.desai@acmecorp.com", "+91-90111-10011",
				"Software Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 82000.0,
				LocalDate.of(2019, 5, 27), true));

		list.add(emp(43L, "EMP-0043", "Sandeep", "Kulkarni", "sandeep.kulkarni@acmecorp.com", "+91-90111-10012",
				"Architect", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 118000.0,
				LocalDate.of(2016, 12, 19), true));

		// — Under Manager Sneha Kulkarni (id=11) — Engineering, Nagpur, India —
		list.add(emp(44L, "EMP-0044", "Chaitra", "Patil", "chaitra.patil@acmecorp.com", "+91-90111-10013",
				"Senior Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 96000.0,
				LocalDate.of(2018, 8, 3), true));

		list.add(emp(45L, "EMP-0045", "Harsh", "Mehta", "harsh.mehta@acmecorp.com", "+91-90111-10014",
				"Software Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 82000.0,
				LocalDate.of(2019, 4, 22), true));

		list.add(emp(46L, "EMP-0046", "Megha", "Sharma", "megha.sharma@acmecorp.com", "+91-90111-10015",
				"Lead Engineer", "Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 109000.0,
				LocalDate.of(2017, 11, 14), true));

		list.add(emp(47L, "EMP-0047", "Nitin", "Rao", "nitin.rao@acmecorp.com", "+91-90111-10016", "Software Engineer",
				"Engineering", "Nagpur", "Maharashtra", "India", 11L, "Sneha Kulkarni", 80000.0,
				LocalDate.of(2020, 6, 1), false));

		// — Under Manager Kiran Verma (id=12) — Engineering, Hyderabad, India —
		list.add(emp(48L, "EMP-0048", "Harini", "Reddy", "harini.reddy@acmecorp.com", "+91-90111-10017",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 94000.0,
				LocalDate.of(2018, 10, 8), true));

		list.add(emp(49L, "EMP-0049", "Rakesh", "Naidu", "rakesh.naidu@acmecorp.com", "+91-90111-10018",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 81000.0,
				LocalDate.of(2019, 12, 2), true));

		list.add(emp(50L, "EMP-0050", "Sowmya", "Iyer", "sowmya.iyer@acmecorp.com", "+91-90111-10019", "Architect",
				"Engineering", "Hyderabad", "Telangana", "India", 12L, "Kiran Verma", 116000.0,
				LocalDate.of(2017, 3, 25), true));

		// — Under Manager Harish Gupta (id=13) — Engineering, Noida, India —
		list.add(emp(51L, "EMP-0051", "Daivik", "Sharma", "daivik.sharma@acmecorp.com", "+91-90111-10020",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 79000.0,
				LocalDate.of(2020, 4, 13), true));

		list.add(emp(52L, "EMP-0052", "Aakash", "Agarwal", "aakash.agarwal@acmecorp.com", "+91-90111-10021",
				"Senior Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 92000.0,
				LocalDate.of(2019, 2, 18), true));

		list.add(emp(53L, "EMP-0053", "Rohit", "Bansal", "rohit.bansal@acmecorp.com", "+91-90111-10022",
				"Lead Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 107000.0,
				LocalDate.of(2018, 6, 25), true));

		list.add(emp(54L, "EMP-0054", "Pallavi", "Mishra", "pallavi.mishra@acmecorp.com", "+91-90111-10023",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 85000.0,
				LocalDate.of(2019, 10, 7), true));

		list.add(emp(55L, "EMP-0055", "Vivek", "Srivastava", "vivek.srivastava@acmecorp.com", "+91-90111-10024",
				"Software Engineer", "Engineering", "Noida", "Uttar Pradesh", "India", 13L, "Harish Gupta", 83000.0,
				LocalDate.of(2020, 8, 3), false));

		// — Under Manager Rakesh Gupta (id=17) — Finance, Mumbai, India —
		list.add(emp(56L, "EMP-0056", "Kavita", "Shah", "kavita.shah@acmecorp.com", "+91-90111-10025",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 72000.0,
				LocalDate.of(2019, 3, 11), true));

		list.add(emp(57L, "EMP-0057", "Rahul", "Kulkarni", "rahul.kulkarni@acmecorp.com", "+91-90111-10026",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 75000.0,
				LocalDate.of(2018, 9, 24), true));

		list.add(emp(58L, "EMP-0058", "Nisha", "Patel", "nisha.patel@acmecorp.com", "+91-90111-10027",
				"Finance Analyst", "Finance", "Mumbai", "Maharashtra", "India", 17L, "Rakesh Gupta", 73000.0,
				LocalDate.of(2020, 2, 17), true));

		// — Under Manager Swathi Rao (id=18) — Finance, Pune, India —
		list.add(emp(59L, "EMP-0059", "Manoj", "Patil", "manoj.patil@acmecorp.com", "+91-90111-10028",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 71000.0,
				LocalDate.of(2019, 6, 3), true));

		list.add(emp(60L, "EMP-0060", "Aarti", "Kulkarni", "aarti.kulkarni@acmecorp.com", "+91-90111-10029",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 74000.0,
				LocalDate.of(2018, 12, 10), true));

		list.add(emp(61L, "EMP-0061", "Naveen", "Joshi", "naveen.joshi@acmecorp.com", "+91-90111-10030",
				"Finance Analyst", "Finance", "Pune", "Maharashtra", "India", 18L, "Swathi Rao", 70000.0,
				LocalDate.of(2020, 5, 19), false));

		// — Under Manager Suresh Kumar (id=19) — Operations, Chennai, India —
		list.add(emp(62L, "EMP-0062", "Meera", "Pillai", "meera.pillai@acmecorp.com", "+91-90111-10031",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 65000.0,
				LocalDate.of(2019, 8, 5), true));

		list.add(emp(63L, "EMP-0063", "Sanjay", "Nair", "sanjay.nair@acmecorp.com", "+91-90111-10032",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 67000.0,
				LocalDate.of(2018, 11, 19), true));

		list.add(emp(64L, "EMP-0064", "Divya", "Krishnamurthy", "divya.krishnamurthy@acmecorp.com", "+91-90111-10033",
				"Support Engineer", "Operations", "Chennai", "Tamil Nadu", "India", 19L, "Suresh Kumar", 66000.0,
				LocalDate.of(2020, 1, 27), true));

		// — Under Manager Pooja Singh (id=20) — Operations, New Delhi, India —
		list.add(emp(65L, "EMP-0065", "Amit", "Tiwari", "amit.tiwari@acmecorp.com", "+91-90111-10034",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 64000.0,
				LocalDate.of(2019, 11, 4), true));

		list.add(emp(66L, "EMP-0066", "Neha", "Sharma", "neha.sharma@acmecorp.com", "+91-90111-10035",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 65000.0,
				LocalDate.of(2018, 5, 8), true));

		list.add(emp(67L, "EMP-0067", "Rohit", "Chauhan", "rohit.chauhan@acmecorp.com", "+91-90111-10036",
				"Support Engineer", "Operations", "New Delhi", "Delhi", "India", 20L, "Pooja Singh", 63000.0,
				LocalDate.of(2020, 9, 14), false));

		// — Under Manager Meera Nambiar (id=21) — Legal, Kochi, India —
		list.add(emp(68L, "EMP-0068", "Anand", "Menon", "anand.menon@acmecorp.com", "+91-90111-10037", "Legal Analyst",
				"Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 78000.0, LocalDate.of(2019, 4, 16), true));

		list.add(emp(69L, "EMP-0069", "Keerthi", "Nair", "keerthi.nair@acmecorp.com", "+91-90111-10038",
				"Legal Analyst", "Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 76000.0,
				LocalDate.of(2018, 10, 3), true));

		list.add(emp(70L, "EMP-0070", "Arvind", "Bhat", "arvind.bhat@acmecorp.com", "+91-90111-10039", "Legal Analyst",
				"Legal", "Kochi", "Kerala", "India", 21L, "Meera Nambiar", 79000.0, LocalDate.of(2020, 7, 20), true));

		// — Under Manager Nitin Bhat (id=22) — Legal, Mangaluru, India —
		list.add(emp(71L, "EMP-0071", "Anjali", "Shetty", "anjali.shetty@acmecorp.com", "+91-90111-10040",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 77000.0,
				LocalDate.of(2019, 1, 7), true));

		list.add(emp(72L, "EMP-0072", "Karthik", "Hegde", "karthik.hegde@acmecorp.com", "+91-90111-10041",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 75000.0,
				LocalDate.of(2018, 6, 14), true));

		list.add(emp(73L, "EMP-0073", "Pavithra", "Rao", "pavithra.rao@acmecorp.com", "+91-90111-10042",
				"Legal Analyst", "Legal", "Mangaluru", "Karnataka", "India", 22L, "Nitin Bhat", 76000.0,
				LocalDate.of(2020, 3, 25), false));

		// — Under Manager Tarun Shah (id=26) — Sales, Ahmedabad, India —
		list.add(emp(74L, "EMP-0074", "Ritesh", "Patel", "ritesh.patel@acmecorp.com", "+91-90111-10043",
				"Sales Executive", "Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 68000.0,
				LocalDate.of(2019, 9, 9), true));

		list.add(emp(75L, "EMP-0075", "Neha", "Desai", "neha.desai@acmecorp.com", "+91-90111-10044", "Sales Executive",
				"Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 70000.0, LocalDate.of(2018, 4, 2), true));

		list.add(emp(76L, "EMP-0076", "Jay", "Mehta", "jay.mehta@acmecorp.com", "+91-90111-10045", "Sales Executive",
				"Sales", "Ahmedabad", "Gujarat", "India", 26L, "Tarun Shah", 67000.0, LocalDate.of(2020, 11, 10),
				true));

		// — Under Manager Bhavna Patel (id=27) — Sales, Surat, India —
		list.add(emp(77L, "EMP-0077", "Dhruv", "Shah", "dhruv.shah@acmecorp.com", "+91-90111-10046", "Sales Executive",
				"Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 69000.0, LocalDate.of(2019, 7, 21), true));

		list.add(emp(78L, "EMP-0078", "Riya", "Joshi", "riya.joshi@acmecorp.com", "+91-90111-10047", "Sales Executive",
				"Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 71000.0, LocalDate.of(2018, 2, 6), true));

		list.add(emp(79L, "EMP-0079", "Yash", "Trivedi", "yash.trivedi@acmecorp.com", "+91-90111-10048",
				"Sales Executive", "Sales", "Surat", "Gujarat", "India", 27L, "Bhavna Patel", 68000.0,
				LocalDate.of(2020, 10, 12), false));

		// — Under Manager Kavya Reddy (id=28) — HR, Bengaluru, India —
		list.add(emp(80L, "EMP-0080", "Shruti", "Bhat", "shruti.bhat@acmecorp.com", "+91-90111-10049", "HR Executive",
				"HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 62000.0, LocalDate.of(2019, 5, 30), true));

		list.add(emp(81L, "EMP-0081", "Tejas", "Kulkarni", "tejas.kulkarni@acmecorp.com", "+91-90111-10050",
				"HR Executive", "HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 63000.0,
				LocalDate.of(2018, 9, 17), true));

		list.add(emp(82L, "EMP-0082", "Pallavi", "Gaikwad", "pallavi.gaikwad@acmecorp.com", "+91-90111-10051",
				"HR Executive", "HR", "Bengaluru", "Karnataka", "India", 28L, "Kavya Reddy", 61000.0,
				LocalDate.of(2020, 4, 6), true));

		// — Under Manager Ravi Iyer (id=29) — Marketing, Hyderabad, India —
		list.add(emp(83L, "EMP-0083", "Chitra", "Narayanan", "chitra.narayanan@acmecorp.com", "+91-90111-10052",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 65000.0,
				LocalDate.of(2019, 3, 24), true));

		list.add(emp(84L, "EMP-0084", "Vishal", "Kapoor", "vishal.kapoor@acmecorp.com", "+91-90111-10053",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 66000.0,
				LocalDate.of(2018, 8, 11), true));

		list.add(emp(85L, "EMP-0085", "Priyanka", "Rao", "priyanka.rao@acmecorp.com", "+91-90111-10054",
				"Marketing Specialist", "Marketing", "Hyderabad", "Telangana", "India", 29L, "Ravi Iyer", 64000.0,
				LocalDate.of(2020, 6, 8), true));

		// — Under Manager Aishwarya Kulkarni (id=30) — Support, Pune, India —
		list.add(emp(86L, "EMP-0086", "Rahul", "Jadhav", "rahul.jadhav@acmecorp.com", "+91-90111-10055",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 67000.0,
				LocalDate.of(2019, 2, 14), true));

		list.add(emp(87L, "EMP-0087", "Snehal", "Patil", "snehal.patil@acmecorp.com", "+91-90111-10056",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 68000.0,
				LocalDate.of(2018, 7, 28), true));

		list.add(emp(88L, "EMP-0088", "Mahesh", "Pawar", "mahesh.pawar@acmecorp.com", "+91-90111-10057",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 65000.0,
				LocalDate.of(2020, 1, 20), true));

		list.add(emp(89L, "EMP-0089", "Aniket", "Shinde", "aniket.shinde@acmecorp.com", "+91-90111-10058",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 64000.0,
				LocalDate.of(2021, 3, 1), true));

		// — Under Manager Prashant Jain (id=31) — Security, Jaipur, India —
		list.add(emp(90L, "EMP-0090", "Gaurav", "Mathur", "gaurav.mathur@acmecorp.com", "+91-90111-10059",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 88000.0,
				LocalDate.of(2019, 6, 10), true));

		list.add(emp(91L, "EMP-0091", "Nidhi", "Sharma", "nidhi.sharma@acmecorp.com", "+91-90111-10060",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 87000.0,
				LocalDate.of(2018, 11, 5), true));

		list.add(emp(92L, "EMP-0092", "Saurabh", "Agarwal", "saurabh.agarwal@acmecorp.com", "+91-90111-10061",
				"Security Analyst", "Security", "Jaipur", "Rajasthan", "India", 31L, "Prashant Jain", 86000.0,
				LocalDate.of(2020, 8, 17), false));

		// ── Additional employees to reach 100 ─────────────────────────────────

		// More Engineers under Vikram Rao (id=8)
		list.add(emp(93L, "EMP-0093", "Rahul", "Mehta", "rahul.mehta@acmecorp.com", "+91-90111-10062",
				"Software Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 77000.0,
				LocalDate.of(2021, 2, 15), true));

		list.add(emp(94L, "EMP-0094", "Poornima", "Rajan", "poornima.rajan@acmecorp.com", "+91-90111-10063",
				"Senior Engineer", "Engineering", "Bengaluru", "Karnataka", "India", 8L, "Vikram Rao", 96000.0,
				LocalDate.of(2020, 10, 19), true));

		// More Engineers under Deepa Menon (id=9)
		list.add(emp(95L, "EMP-0095", "Varun", "Bhatt", "varun.bhatt@acmecorp.com", "+91-90111-10064",
				"Software Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 78000.0,
				LocalDate.of(2021, 5, 3), true));

		list.add(emp(96L, "EMP-0096", "Shweta", "Pandey", "shweta.pandey@acmecorp.com", "+91-90111-10065",
				"Senior Engineer", "Engineering", "Hyderabad", "Telangana", "India", 9L, "Deepa Menon", 94000.0,
				LocalDate.of(2020, 12, 7), true));

		// More Engineers under Amit Joshi (id=10)
		list.add(emp(97L, "EMP-0097", "Sandhya", "Kulkarni", "sandhya.kulkarni@acmecorp.com", "+91-90111-10066",
				"Software Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 83000.0,
				LocalDate.of(2021, 1, 11), true));

		list.add(emp(98L, "EMP-0098", "Pradeep", "Jain", "pradeep.jain@acmecorp.com", "+91-90111-10067",
				"Senior Engineer", "Engineering", "Pune", "Maharashtra", "India", 10L, "Amit Joshi", 99000.0,
				LocalDate.of(2020, 7, 27), true));

		// More Support Engineers under Aishwarya Kulkarni (id=30)
		list.add(emp(99L, "EMP-0099", "Yogesh", "Patil", "yogesh.patil@acmecorp.com", "+91-90111-10068",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 66000.0,
				LocalDate.of(2021, 4, 19), true));

		list.add(emp(100L, "EMP-0100", "Tina", "Joshi", "tina.joshi@acmecorp.com", "+91-90111-10069",
				"Support Engineer", "Support", "Pune", "Maharashtra", "India", 30L, "Aishwarya Kulkarni", 65000.0,
				LocalDate.of(2021, 9, 6), true));

		return list;
	}

	/**
	 * Factory method to build an {@link Employee} instance from individual fields.
	 * Computes {@code fullName} automatically from first and last name.
	 */
	private static Employee emp(Long id, String empNum, String firstName, String lastName, String email, String phone,
			String designation, String department, String city, String state, String country, Long managerId,
			String managerName, Double salary, LocalDate joiningDate, boolean active) {

		Employee emp = new Employee();

		emp.setId(id);
		emp.setEmployeeNumber(empNum);
		emp.setFirstName(firstName);
		emp.setLastName(lastName);
		emp.setFullName(firstName + " " + lastName);
		emp.setEmail(email);
		emp.setPhone(phone);
		emp.setDesignation(designation);
		emp.setDepartment(department);
		emp.setCity(city);
		emp.setState(state);
		emp.setCountry(country);
		emp.setManagerId(managerId);
		emp.setManagerName(managerName);
		emp.setSalary(salary);
		emp.setJoiningDate(joiningDate);
		emp.setActive(active);

		return emp;
	}
}

   

Step 8: Implementing MCP Resources.

Now that our MCP server is configured, it's time to expose data that AI clients can read.

 

In MCP, Resources represent read-only information identified by a unique URI. Unlike Tools, resources do not execute business logic or require user input. Instead, they expose static or semi-static data that AI clients can retrieve whenever needed.

 

Think of an MCP Resource as the equivalent of a REST GET endpoint. An MCP server exposes resources using URI schemes such as:

 

·      employees://all

·      employees://departments

·      employees://countries

·      employees://statistics

 

An AI client simply asks the server to read one of these resources, and the server returns the corresponding data.

 

What are we building?

Our Employee MCP Server exposes 15 resources, each representing a different view of the employee dataset.

 

Resource URI              

Description                    

employees://all

Complete employee list         

employees://countries

All countries                  

employees://cities

All office locations           

employees://departments

Available departments          

employees://organization

Complete organization hierarchy

employees://managers

All managers                   

employees://statistics

Employee statistics            

employees://engineering

Engineering department         

employees://sales

Sales department               

employees://finance

Finance department             

employees://hr

Human Resources                

employees://marketing

Marketing department           

employees://support

Support department             

employees://india

Employees in India             

employees://usa

Employees in USA               

 

Instead of implementing REST controllers, Spring AI registers these resources directly with the MCP server.

 

How to register all the resources?

Once we define individual resources, the next step is registering them with the MCP server.

 

In Spring AI, resource registration is extremely simple. We create a Spring @Bean that returns a List<SyncResourceSpecification>. During application startup, Spring AI automatically discovers this bean and registers every resource with the MCP server.

 

@Bean
public List<McpStatelessServerFeatures.SyncResourceSpecification> employeeResources() {
    return List.of(
            allEmployeesResource(),
            countriesResource(),
            citiesResource(),
            departmentsResource(),
            organizationResource(),
            managersResource(),
            statisticsResource(),
            engineeringResource(),
            salesResource(),
            financeResource(),
            hrResource(),
            marketingResource(),
            supportResource(),
            indiaResource(),
            usaResource()
    );
}

   

Sample implementation of allEmployeesResource looks like below.

 

  

private McpStatelessServerFeatures.SyncResourceSpecification allEmployeesResource() {
        var resource = McpSchema.Resource.builder().uri("employees://all").name("All Employees")
                .description("Complete list of all 100 employees in the organization.").mimeType("application/json")
                .build();

        return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
            log.debug("[RESOURCE] Reading employees://all");
            List<Employee> employees = employeeService.findAll();
            return toResourceResult(req.uri(), employees);
        });
    }

   

How does this work?

Each method in the list returns an instance of SyncResourceSpecification. When Spring Boot starts:

 

·      Spring creates the EmployeeResourceRegistrar configuration class.

·      It invokes the employeeResources() bean method.

·      The method returns a list containing all 15 resource specifications.

·      Spring AI's MCP auto-configuration detects this bean.

·      Each SyncResourceSpecification is registered with the MCP server.

·      The resources become available to any MCP client.

 

The registration flow looks like this:

 

Spring Boot Startup
        │
        ▼
EmployeeResourceRegistrar
        │
        ▼
@Bean employeeResources()
        │
        ▼
List<SyncResourceSpecification>
        │
        ▼
Spring AI MCP Auto-Configuration
        │
        ▼
Registers each Resource
        │
        ▼
MCP Server
        │
        ▼
employees://all
employees://countries
employees://departments
...
employees://usa

   

Why to return a List instead of multiple beans?

Returning a single list keeps all resource registrations centralized in one place. It offers several advantages:

 

·      Easy to discover: All resources are defined in a single configuration class.

·      Simpler maintenance: Adding or removing a resource only requires updating one method.

·      Cleaner code: Avoids creating 15 separate @Bean methods.

·      Automatic registration: Spring AI registers every resource in the list without any additional configuration.

 

This pattern scales well as your MCP server grows. Whether you have 5 resources or 100, you only need to add the new SyncResourceSpecification to the list, and Spring AI takes care of the rest.

 

EmployeeResourceRegistrar.java

package com.sample.app.resources;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sample.app.model.Employee;
import com.sample.app.service.EmployeeService;

import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;

/**
 * Registers all MCP Resources for the Employee MCP Server.
 *
 * <p>
 * <b>What are MCP Resources?</b> Resources are static or semi-static data
 * endpoints identified by a URI scheme. Unlike tools (which are callable
 * functions), resources represent data the AI can "read" without providing
 * parameters. Think of them as bookmarks to pre-aggregated views.
 *
 * <p>
 * <b>How registration works in STATELESS mode:</b>
 * <ol>
 * <li>Each resource is a
 * {@link McpStatelessServerFeatures.SyncResourceSpecification}</li>
 * <li>It combines a {@link McpSchema.Resource} (URI + metadata) with a read
 * handler</li>
 * <li>The read handler:
 * {@code BiFunction<McpTransportContext, ReadResourceRequest, ReadResourceResult>}</li>
 * <li>We expose all registrations as a single {@code List<>} bean — the
 * autoconfigure picks it up</li>
 * </ol>
 *
 * <p>
 * <b>Resources registered (15 total):</b>
 * <ul>
 * <li>{@code employees://all} — Full employee list</li>
 * <li>{@code employees://countries} — All countries</li>
 * <li>{@code employees://cities} — All cities</li>
 * <li>{@code employees://departments} — All departments</li>
 * <li>{@code employees://organization} — Org hierarchy</li>
 * <li>{@code employees://managers} — All managers</li>
 * <li>{@code employees://statistics} — Aggregate statistics</li>
 * <li>{@code employees://engineering} — Engineering dept</li>
 * <li>{@code employees://sales} — Sales dept</li>
 * <li>{@code employees://finance} — Finance dept</li>
 * <li>{@code employees://hr} — HR dept</li>
 * <li>{@code employees://marketing} — Marketing dept</li>
 * <li>{@code employees://support} — Support dept</li>
 * <li>{@code employees://india} — India employees</li>
 * <li>{@code employees://usa} — USA employees</li>
 * </ul>
 */

@Configuration
public class EmployeeResourceRegistrar {

	private static final Logger log = LoggerFactory.getLogger(EmployeeResourceRegistrar.class);

	private final EmployeeService employeeService;

	/**
	 * ObjectMapper configured to handle Java 8 date/time types (LocalDate).
	 * Resources serialize their content as JSON text manually.
	 */
	private final ObjectMapper objectMapper;

	public EmployeeResourceRegistrar(EmployeeService employeeService) {
		this.employeeService = employeeService;
		// Configure Jackson to handle LocalDate without timestamps
		this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
				.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
	}

	/**
	 * Registers all 15 MCP Resource specifications as a single bean.
	 *
	 * <p>
	 * The Spring AI autoconfigure reads this bean via
	 * {@code ObjectProvider<List<...>>} and registers each specification with the
	 * stateless MCP server.
	 *
	 * @return list of resource specifications
	 */
	@Bean
	public List<McpStatelessServerFeatures.SyncResourceSpecification> employeeResources() {
		return List.of(allEmployeesResource(), countriesResource(), citiesResource(), departmentsResource(),
				organizationResource(), managersResource(), statisticsResource(), engineeringResource(),
				salesResource(), financeResource(), hrResource(), marketingResource(), supportResource(),
				indiaResource(), usaResource());
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Resource definitions
	// ─────────────────────────────────────────────────────────────────────────

	private McpStatelessServerFeatures.SyncResourceSpecification allEmployeesResource() {
		var resource = McpSchema.Resource.builder().uri("employees://all").name("All Employees")
				.description("Complete list of all 100 employees in the organization.").mimeType("application/json")
				.build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://all");
			List<Employee> employees = employeeService.findAll();
			return toResourceResult(req.uri(), employees);
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification countriesResource() {
		var resource = McpSchema.Resource.builder().uri("employees://countries").name("Countries")
				.description("All unique countries where the organization has employees.").mimeType("application/json")
				.build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://countries");
			List<String> countries = employeeService.getCountries();
			return toResourceResult(req.uri(), Map.of("countries", countries, "count", countries.size()));
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification citiesResource() {
		var resource = McpSchema.Resource.builder().uri("employees://cities").name("Cities")
				.description("All unique cities where the organization has offices.").mimeType("application/json")
				.build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://cities");
			List<String> cities = employeeService.getCities();
			return toResourceResult(req.uri(), Map.of("cities", cities, "count", cities.size()));
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification departmentsResource() {
		var resource = McpSchema.Resource.builder().uri("employees://departments").name("Departments")
				.description("All unique department names in the organization.").mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://departments");
			List<String> depts = employeeService.getDepartments();
			return toResourceResult(req.uri(), Map.of("departments", depts, "count", depts.size()));
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification organizationResource() {
		var resource = McpSchema.Resource.builder().uri("employees://organization").name("Organization Hierarchy")
				.description("Full organization hierarchy tree from CEO down to individual contributors.")
				.mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://organization");
			Map<String, Object> hierarchy = employeeService.getOrganizationHierarchy();
			return toResourceResult(req.uri(), hierarchy);
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification managersResource() {
		var resource = McpSchema.Resource.builder().uri("employees://managers").name("All Managers")
				.description("All employees who manage at least one direct report (CEO, VPs, Directors, Managers).")
				.mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://managers");
			List<Employee> managers = employeeService.getManagers();
			return toResourceResult(req.uri(), managers);
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification statisticsResource() {
		var resource = McpSchema.Resource.builder().uri("employees://statistics").name("Employee Statistics")
				.description(
						"Aggregate statistics: total headcount, active/inactive split, per-country and per-department counts.")
				.mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading employees://statistics");

			var count = employeeService.countEmployees();
			var byCountry = employeeService.countEmployeesByCountry();
			var byDept = employeeService.countEmployeesByDepartment();

			Map<String, Object> stats = Map.of("totalEmployees", count.getTotalEmployees(), "activeEmployees",
					count.getActiveEmployees(), "inactiveEmployees", count.getInactiveEmployees(), "byCountry",
					byCountry, "byDepartment", byDept);
			return toResourceResult(req.uri(), stats);
		});
	}

	private McpStatelessServerFeatures.SyncResourceSpecification engineeringResource() {
		return departmentResource("employees://engineering", "Engineering Department",
				"All employees in the Engineering department.", "Engineering");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification salesResource() {
		return departmentResource("employees://sales", "Sales Department", "All employees in the Sales department.",
				"Sales");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification financeResource() {
		return departmentResource("employees://finance", "Finance Department",
				"All employees in the Finance department.", "Finance");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification hrResource() {
		return departmentResource("employees://hr", "HR Department", "All employees in the Human Resources department.",
				"HR");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification marketingResource() {
		return departmentResource("employees://marketing", "Marketing Department",
				"All employees in the Marketing department.", "Marketing");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification supportResource() {
		return departmentResource("employees://support", "Support Department",
				"All employees in the Support department.", "Support");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification indiaResource() {
		return countryResource("employees://india", "India Employees", "All employees based in India.", "India");
	}

	private McpStatelessServerFeatures.SyncResourceSpecification usaResource() {
		return countryResource("employees://usa", "USA Employees", "All employees based in the United States.", "USA");
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Helper builders
	// ─────────────────────────────────────────────────────────────────────────

	/**
	 * Creates a resource specification that returns employees filtered by
	 * department.
	 */
	private McpStatelessServerFeatures.SyncResourceSpecification departmentResource(String uri, String name,
			String description, String department) {

		var resource = McpSchema.Resource.builder().uri(uri).name(name).description(description)
				.mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading {} (dept={})", uri, department);
			List<Employee> employees = employeeService.findByDepartment(department);
			return toResourceResult(req.uri(),
					Map.of("department", department, "count", employees.size(), "employees", employees));
		});
	}

	/**
	 * Creates a resource specification that returns employees filtered by country.
	 */
	private McpStatelessServerFeatures.SyncResourceSpecification countryResource(String uri, String name,
			String description, String country) {

		var resource = McpSchema.Resource.builder().uri(uri).name(name).description(description)
				.mimeType("application/json").build();

		return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (ctx, req) -> {
			log.debug("[RESOURCE] Reading {} (country={})", uri, country);
			List<Employee> employees = employeeService.findByCountry(country);
			return toResourceResult(req.uri(),
					Map.of("country", country, "count", employees.size(), "employees", employees));
		});
	}

	/**
	 * Serializes any object to JSON and wraps it in an MCP
	 * {@link McpSchema.ReadResourceResult}.
	 *
	 * <p>
	 * Resources must return text content (unlike tools which return typed DTOs). We
	 * serialize manually here using Jackson, then wrap in
	 * {@link McpSchema.TextResourceContents}.
	 *
	 * @param uri  the resource URI (echoed back in the response)
	 * @param data the object to serialize
	 * @return a ReadResourceResult containing the JSON text
	 */
	private McpSchema.ReadResourceResult toResourceResult(String uri, Object data) {
		try {
			String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data);
			var contents = new McpSchema.TextResourceContents(uri, "application/json", json);
			return new McpSchema.ReadResourceResult(List.of(contents));
		} catch (JsonProcessingException e) {
			log.error("[RESOURCE] JSON serialization failed for uri={}: {}", uri, e.getMessage());
			var error = new McpSchema.TextResourceContents(uri, "application/json",
					"{\"error\": \"Serialization failed: " + e.getMessage() + "\"}");
			return new McpSchema.ReadResourceResult(List.of(error));
		}
	}
}

   

Step 9: Implementing MCP Prompts

 

So far, we've exposed  Resources that provide read-only data. The second capability supported by MCP is Prompts. Unlike tools and resources, prompts don't execute code or return datasets. Instead, they provide reusable conversation templates that help AI models generate consistent, structured, and context-aware responses.

 

Think of prompts as reusable AI instructions that can be shared across multiple AI clients.

 

Why do we need MCP Prompts?

Suppose a user asks: "Generate a professional profile for employee 42."

 

Without prompts, every AI client would have to write its own instructions:

 

·      What tone should be used?

·      Should the response contain bullet points?

·      Should it include responsibilities?

·      Should it mention career growth?

·      Should it summarize skills?

 

Different AI clients might generate completely different responses. With an MCP Prompt, all clients reuse the same template, ensuring consistent output regardless of which AI client is connected.

 

Instead of every client reinventing the prompt, the server becomes the single source of truth.

 

Why multiple prompts?

A common question is, Why not create one large prompt instead of multiple prompts?

 

Because different business scenarios require different AI behavior. Imagine an HR application. Sometimes the user wants:

 

·      an employee profile

·      a department summary

·      a manager overview

·      workforce statistics

·      help searching employees

·      organization navigation

 

Each scenario requires different instructions and different context. Instead of creating one massive prompt containing everything, we create focused prompts, each solving one specific problem.

 

Our Employee MCP Server exposes seven reusable prompts.

 

Prompt

Purpose

employee-summary

Generates a professional employee profile      

department-summary

Creates a department overview                  

country-summary

Summarizes employees within a country          

manager-summary

Generates a manager's team profile

hr-assistant

Establishes an HR assistant persona

employee-search-assistant

Guides AI in selecting the correct search tools

organization-explorer

Helps navigate the organizational hierarchy    

Each prompt represents a different AI capability.

 

When are prompts used?

Unlike tools, prompts are not automatically executed. An AI client first discovers the available prompts from the MCP server. When the user requests a particular task, the client selects the appropriate prompt.

 

Registering All Prompts

Prompt registration is almost identical to resource registration. We expose a Spring bean that returns a list of prompt specifications.

@Bean
public List<McpStatelessServerFeatures.SyncPromptSpecification> employeePrompts() {
    return List.of(
            employeeSummaryPrompt(),
            departmentSummaryPrompt(),
            countrySummaryPrompt(),
            managerSummaryPrompt(),
            hrAssistantPrompt(),
            employeeSearchAssistantPrompt(),
            organizationExplorerPrompt()
    );
}

   

Each method returns one SyncPromptSpecification. During application startup, Spring AI automatically discovers this bean and registers every prompt with the MCP server. No additional configuration is required.

 

EmployeePromptRegistrar.java

package com.sample.app.prompts;

import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.sample.app.model.Employee;
import com.sample.app.service.EmployeeService;

/**
 * Registers all MCP Prompts for the Employee MCP Server.
 *
 * <p>
 * <b>What are MCP Prompts?</b> Prompts are reusable message templates that
 * guide AI behavior. Unlike tools (which execute code) or resources (which
 * return static data), prompts generate structured conversation messages that
 * tell the AI <em>how to behave</em> for a given context.
 *
 * <p>
 * <b>When the AI calls a prompt:</b>
 * <ol>
 * <li>The client sends a {@code prompts/get} request with the prompt name and
 * arguments</li>
 * <li>The handler uses arguments to build dynamic context (looking up employee
 * data)</li>
 * <li>It returns a list of {@link McpSchema.PromptMessage} objects (SYSTEM +
 * USER messages)</li>
 * <li>The AI uses these messages as the starting context for its response</li>
 * </ol>
 *
 * <p>
 * <b>Prompts registered (7 total):</b>
 * <ul>
 * <li>{@code employee-summary} — Professional summary for a specific
 * employee</li>
 * <li>{@code department-summary} — Overview of a department's team</li>
 * <li>{@code country-summary} — Geographic workforce summary</li>
 * <li>{@code manager-summary} — Manager's team overview</li>
 * <li>{@code hr-assistant} — Reusable HR assistant persona</li>
 * <li>{@code employee-search-assistant} — Guided search assistant</li>
 * <li>{@code organization-explorer} — Org hierarchy navigation assistant</li>
 * </ul>
 */
@Configuration
public class EmployeePromptRegistrar {
        private static final Logger log = LoggerFactory.getLogger(EmployeePromptRegistrar.class);

        private final EmployeeService employeeService;

        public EmployeePromptRegistrar(EmployeeService employeeService) {
                this.employeeService = employeeService;
        }

        /**
         * Registers all 7 MCP Prompt specifications as a single bean.
         *
         * @return list of prompt specifications
         */
        @Bean
        public List<McpStatelessServerFeatures.SyncPromptSpecification> employeePrompts() {
                return List.of(employeeSummaryPrompt(), departmentSummaryPrompt(), countrySummaryPrompt(),
                                managerSummaryPrompt(), hrAssistantPrompt(), employeeSearchAssistantPrompt(),
                                organizationExplorerPrompt());
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 1 — Employee Summary
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Generates a professional summary prompt for a specific employee. Looks up the
         * employee by ID and fills in real data.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification employeeSummaryPrompt() {

                var prompt = new McpSchema.Prompt("employee-summary", "Generate a professional summary for a specific employee",
                                List.of(new McpSchema.PromptArgument("employeeId", "The numeric ID of the employee (e.g., 42)",
                                                Boolean.TRUE)));

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        String employeeIdStr = getArg(req.arguments(), "employeeId", "1");
                        Long employeeId = Long.parseLong(employeeIdStr.trim());

                        log.debug("[PROMPT] employee-summary for employeeId={}", employeeId);

                        Optional<Employee> employeeOpt = employeeService.findById(employeeId);
                        if (employeeOpt.isEmpty()) {
                                return errorPromptResult("Employee with ID " + employeeId + " not found.");
                        }
                        Employee emp = employeeOpt.get();

                        String systemMessage = """
                                        You are an experienced HR professional generating employee profiles.
                                        Write in a formal, positive tone. Structure your response as:
                                        1. Professional Summary (2-3 sentences)
                                        2. Role & Responsibilities (bullet points)
                                        3. Skills Overview (inferred from designation)
                                        4. Career Path (based on hierarchy level)
                                        """;

                        String userMessage = String.format("""
                                        Generate a professional summary for the following employee:

                                        Name:        %s
                                        Employee #:  %s
                                        Designation: %s
                                        Department:  %s
                                        Location:    %s, %s, %s
                                        Manager:     %s
                                        Joined:      %s
                                        Status:      %s

                                        Please provide a structured professional summary.
                                        """, emp.getFullName(), emp.getEmployeeNumber(), emp.getDesignation(), emp.getDepartment(),
                                        emp.getCity(), emp.getState(), emp.getCountry(),
                                        emp.getManagerName() != null ? emp.getManagerName() : "N/A (CEO)", emp.getJoiningDate(),
                                        emp.isActive() ? "Active" : "Inactive");

                        return buildPromptResult("Employee Professional Summary for " + emp.getFullName(), systemMessage,
                                        userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 2 — Department Summary
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Generates a department overview prompt with real team data.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification departmentSummaryPrompt() {

                var prompt = new McpSchema.Prompt("department-summary",
                                "Generate a department overview including team size, managers, and distribution",
                                List.of(new McpSchema.PromptArgument("department", "Department name (e.g., Engineering, Sales, HR)",
                                                Boolean.TRUE)));

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        String department = getArg(req.arguments(), "department", "Engineering");
                        log.debug("[PROMPT] department-summary for department={}", department);

                        List<Employee> members = employeeService.findByDepartment(department);
                        if (members.isEmpty()) {
                                return errorPromptResult("Department '" + department
                                                + "' not found. Use getDepartments() to see available departments.");
                        }

                        long active = members.stream().filter(Employee::isActive).count();
                        long inactive = members.size() - active;

                        List<Employee> managers = employeeService.getManagers().stream()
                                        .filter(m -> m.getDepartment().equalsIgnoreCase(department)).collect(Collectors.toList());

                        List<String> cities = members.stream().map(Employee::getCity).distinct().sorted()
                                        .collect(Collectors.toList());
                        List<String> designations = members.stream().map(Employee::getDesignation).distinct().sorted()
                                        .collect(Collectors.toList());

                        String systemMessage = """
                                        You are an HR analyst preparing a department overview report.
                                        Be concise, data-driven, and professional.
                                        Structure your response as:
                                        1. Department Overview
                                        2. Team Size & Composition
                                        3. Leadership
                                        4. Geographic Distribution
                                        5. Key Observations
                                        """;

                        String userMessage = String.format("""
                                        Generate a department summary for: %s

                                        Team Size:    %d total (%d active, %d inactive)
                                        Managers:     %s
                                        Office Locations: %s
                                        Designations: %s

                                        Please provide a structured department overview.
                                        """, department, members.size(), active, inactive,
                                        managers.stream().map(Employee::getFullName).collect(Collectors.joining(", ")),
                                        String.join(", ", cities), String.join(", ", designations));

                        return buildPromptResult("Department Summary: " + department, systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 3 — Country Summary
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Generates a geographic workforce summary for a country.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification countrySummaryPrompt() {

                var prompt = new McpSchema.Prompt("country-summary", "Generate a workforce summary for a specific country", List
                                .of(new McpSchema.PromptArgument("country", "Country name (e.g., India, USA, Germany)", Boolean.TRUE)));

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        String country = getArg(req.arguments(), "country", "India");
                        log.debug("[PROMPT] country-summary for country={}", country);

                        List<Employee> employees = employeeService.findByCountry(country);
                        if (employees.isEmpty()) {
                                return errorPromptResult(
                                                "Country '" + country + "' not found. Use getCountries() to see available countries.");
                        }

                        List<String> depts = employees.stream().map(Employee::getDepartment).distinct().sorted()
                                        .collect(Collectors.toList());
                        List<String> cities = employees.stream().map(Employee::getCity).distinct().sorted()
                                        .collect(Collectors.toList());
                        long active = employees.stream().filter(Employee::isActive).count();

                        String systemMessage = """
                                        You are a Global HR Manager preparing a country workforce report.
                                        Focus on team composition, departments present, and geographic spread.
                                        """;

                        String userMessage = String.format("""
                                        Generate a workforce summary for: %s

                                        Total Employees: %d (%d active)
                                        Departments:     %s
                                        Cities:          %s

                                        Provide insights about the workforce in %s.
                                        """, country, employees.size(), active, String.join(", ", depts), String.join(", ", cities),
                                        country);

                        return buildPromptResult("Country Workforce Summary: " + country, systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 4 — Manager Summary
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Generates a manager's team overview with their direct reports listed.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification managerSummaryPrompt() {

                var prompt = new McpSchema.Prompt("manager-summary",
                                "Generate a team overview for a specific manager including all direct reports",
                                List.of(new McpSchema.PromptArgument("managerId", "The numeric ID of the manager", Boolean.TRUE)));

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        String managerIdStr = getArg(req.arguments(), "managerId", "8");
                        Long managerId = Long.parseLong(managerIdStr.trim());

                        log.debug("[PROMPT] manager-summary for managerId={}", managerId);

                        Optional<Employee> managerOpt = employeeService.findById(managerId);
                        if (managerOpt.isEmpty()) {
                                return errorPromptResult("Manager with ID " + managerId + " not found.");
                        }
                        Employee manager = managerOpt.get();

                        List<Employee> reports;
                        try {
                                reports = employeeService.getEmployeesUnderManager(managerId);
                        } catch (Exception e) {
                                reports = List.of();
                        }

                        List<String> departments = reports.stream().map(Employee::getDepartment).distinct()
                                        .collect(Collectors.toList());

                        String systemMessage = """
                                        You are an HR analyst preparing a manager's team profile.
                                        Summarize the team composition, skills, and geographic spread.
                                        """;

                        String userMessage = String.format("""
                                        Generate a team summary for Manager: %s (%s)

                                        Manager Details:
                                          ID:          %d
                                          Designation: %s
                                          Department:  %s
                                          Location:    %s, %s

                                        Team Size: %d direct reports
                                        Departments in team: %s

                                        Team Members:
                                        %s

                                        Please summarize this manager's team.
                                        """, manager.getFullName(), manager.getEmployeeNumber(), manager.getId(), manager.getDesignation(),
                                        manager.getDepartment(), manager.getCity(), manager.getCountry(), reports.size(),
                                        String.join(", ", departments),
                                        reports.stream()
                                                        .map(e -> "  - " + e.getFullName() + " (" + e.getDesignation() + ", " + e.getCity() + ")")
                                                        .collect(Collectors.joining("\n")));

                        return buildPromptResult("Manager Team Summary: " + manager.getFullName(), systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 5 — HR Assistant (Persona)
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Establishes an HR Assistant persona that answers only from MCP data.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification hrAssistantPrompt() {

                var prompt = new McpSchema.Prompt("hr-assistant",
                                "Activates an HR Assistant that answers questions using only the MCP employee data", List.of() // No
                                                                                                                                                                                                                                // arguments
                                                                                                                                                                                                                                // —
                                                                                                                                                                                                                                // this
                                                                                                                                                                                                                                // is a
                                                                                                                                                                                                                                // persona
                                                                                                                                                                                                                                // prompt
                );

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        log.debug("[PROMPT] hr-assistant activated");

                        var count = employeeService.countEmployees();
                        List<String> depts = employeeService.getDepartments();
                        List<String> countries = employeeService.getCountries();

                        String systemMessage = String.format("""
                                        You are an intelligent HR Assistant for AcmeCorp.
                                        You ONLY answer questions using information available through the MCP resources and tools.
                                        If asked about something outside the employee data, politely decline.

                                        Organization facts you know:
                                        - Total Employees: %d (%d active, %d inactive)
                                        - Departments: %s
                                        - Countries of operation: %s
                                        - Hierarchy: CEO → VP → Director → Manager → Individual Contributors

                                        Available MCP Tools you can call to answer questions:
                                        - getEmployee(id), getEmployees(page, size)
                                        - getEmployeesByDepartment, getEmployeesByCountry, getEmployeesByCity
                                        - getEmployeesUnderManager(managerId)
                                        - searchEmployees(keyword)
                                        - countEmployees(), countEmployeesByCountry(), countEmployeesByDepartment()
                                        - getOrganizationHierarchy()

                                        Always be helpful, accurate, and professional.
                                        """, count.getTotalEmployees(), count.getActiveEmployees(), count.getInactiveEmployees(),
                                        String.join(", ", depts), String.join(", ", countries));

                        String userMessage = "Hello! I'm ready to answer questions about AcmeCorp's employees. What would you like to know?";

                        return buildPromptResult("HR Assistant", systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 6 — Employee Search Assistant
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * A guided assistant that helps AI clients effectively use the search tools.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification employeeSearchAssistantPrompt() {

                var prompt = new McpSchema.Prompt("employee-search-assistant",
                                "Activates a search guide that helps find employees using the right MCP tools", List.of() // No
                                                                                                                                                                                                                        // arguments
                );

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        log.debug("[PROMPT] employee-search-assistant activated");

                        String systemMessage = """
                                        You are an Employee Search Assistant for AcmeCorp.
                                        Your job is to help users find the right employees using the available MCP tools.

                                        Search strategy:
                                        1. If searching by NAME or ROLE: use searchEmployees(keyword)
                                        2. If searching by CITY: use getEmployeesByCity(city, page, size)
                                        3. If searching by COUNTRY: use getEmployeesByCountry(country, page, size)
                                        4. If searching by DEPARTMENT: use getEmployeesByDepartment(dept, page, size)
                                        5. If looking for MANAGERS: use getManagers()
                                        6. If looking for REPORTS: use getEmployeesUnderManager(managerId)
                                        7. If browsing all: use getEmployees(page, size)
                                        8. For single profile: use getEmployee(employeeId)

                                        Always confirm the search type before calling a tool.
                                        If results are too many, suggest narrowing with filters.
                                        Pagination starts at page 0.
                                        """;

                        String userMessage = "I'm ready to help you search for employees. What are you looking for — name, role, location, or department?";

                        return buildPromptResult("Employee Search Assistant", systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Prompt 7 — Organization Explorer
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * An assistant that helps navigate the org hierarchy interactively.
         */
        private McpStatelessServerFeatures.SyncPromptSpecification organizationExplorerPrompt() {

                var prompt = new McpSchema.Prompt("organization-explorer",
                                "Navigate the organization hierarchy interactively from CEO down to individual contributors", List.of() // No
                                                                                                                                                                                                                                                // arguments
                );

                return new McpStatelessServerFeatures.SyncPromptSpecification(prompt, (ctx, req) -> {
                        log.debug("[PROMPT] organization-explorer activated");

                        String systemMessage = """
                                        You are an Organization Explorer for AcmeCorp.
                                        Help users navigate and understand the company's hierarchy.

                                        Hierarchy structure:
                                        Level 1: CEO (Robert Mitchell, id=1)
                                        Level 2: Vice Presidents (Engineering, Finance, Sales — ids 2,3,4)
                                        Level 3: Directors (9 directors — ids 5-7, 14-16, 23-25)
                                        Level 4: Managers (18 managers — ids 8-13, 17-22, 26-31)
                                        Level 5: Individual Contributors (ids 32-100)

                                        Exploration tools:
                                        - getOrganizationHierarchy() : Full tree (use sparingly — large response)
                                        - getManagers()              : All people with direct reports
                                        - getEmployeesUnderManager(id) : Direct reports of any person
                                        - getEmployee(id)            : Profile of any person

                                        Tips for navigation:
                                        - Start with getManagers() to see the leadership map
                                        - Drill down with getEmployeesUnderManager(managerId)
                                        - Use getEmployee(id) for detailed profiles
                                        """;

                        String userMessage = "Ready to explore AcmeCorp's organization chart! Where would you like to start — the executive team, a specific department, or a particular person?";

                        return buildPromptResult("Organization Explorer", systemMessage, userMessage);
                });
        }

        // ─────────────────────────────────────────────────────────────────────────
        // Helper methods
        // ─────────────────────────────────────────────────────────────────────────

        /**
         * Builds a standard two-message prompt result (SYSTEM + USER).
         *
         * @param description short description of the prompt context
         * @param systemText  the SYSTEM role message
         * @param userText    the USER role message
         * @return a {@link McpSchema.GetPromptResult}
         */
        private McpSchema.GetPromptResult buildPromptResult(String description, String systemText, String userText) {
                return new McpSchema.GetPromptResult(description,
                                List.of(new McpSchema.PromptMessage(McpSchema.Role.USER,
                                                new McpSchema.TextContent("SYSTEM: " + systemText.strip())),
                                                new McpSchema.PromptMessage(McpSchema.Role.USER, new McpSchema.TextContent(userText.strip()))));
        }

        /**
         * Builds an error prompt result when a required argument is invalid.
         *
         * @param errorMessage the human-readable error explanation
         * @return a prompt result communicating the error
         */
        private McpSchema.GetPromptResult errorPromptResult(String errorMessage) {
                return new McpSchema.GetPromptResult("Error", List.of(
                                new McpSchema.PromptMessage(McpSchema.Role.USER, new McpSchema.TextContent("Error: " + errorMessage))));
        }

        /**
         * Safely extracts a named argument from the prompt arguments map.
         *
         * <p>
         * Note: {@code McpSchema.GetPromptRequest.arguments()} returns
         * {@code Map<String, Object>} because the MCP spec allows any JSON value as an
         * argument. We coerce to String here.
         *
         * @param arguments    the arguments map from the prompt request (may be null)
         * @param key          the argument key
         * @param defaultValue fallback value if the key is absent or null
         * @return the argument value as a String, or the default
         */
        private String getArg(Map<String, Object> arguments, String key, String defaultValue) {
                if (arguments == null)
                        return defaultValue;
                Object value = arguments.get(key);
                if (value == null)
                        return defaultValue;
                String str = value.toString();
                return !str.isBlank() ? str : defaultValue;
        }
}

   

Prompt Registration Flow

The lifecycle of prompt registration is very similar to resources.

 

Spring Boot Startup
        │
        ▼
EmployeePromptRegistrar
        │
        ▼
@Bean employeePrompts()
        │
        ▼
List<SyncPromptSpecification>
        │
        ▼
Spring AI MCP Auto-Configuration
        │
        ▼
Registers all Prompts
        │
        ▼
MCP Server
        │
        ▼
AI Client can discover prompts

   

Step 10: Registering MCP Tools in Spring AI

Unlike Resources and Prompts, you don't register each tool individually. Spring AI can automatically discover any method annotated with @Tool. All we need to do is expose those methods through a ToolCallbackProvider.

 

How Tool Registration Works ?

The registration process is surprisingly simple.

 

·      Create a Spring bean that contains your business methods.

·      Annotate each callable method with @Tool.

·      Register the bean using MethodToolCallbackProvider.

·      Spring AI scans every @Tool method.

·      It automatically generates JSON Schema for each method.

·      The MCP server exposes them through the tools/list endpoint.

 

Unlike Resources or Prompts, you don't create a separate registration object for every tool. The entire registration is handled by a single configuration bean.

 

Step 1: Create a Tool Class

Create a normal Spring bean.

@Service
public class EmployeeTools {

    private final EmployeeService employeeService;

    public EmployeeTools(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

}

   

This class contains every capability that you want an AI client to invoke.

 

Step 2: Annotate Methods with @Tool

Every callable operation simply uses the @Tool annotation.

@Tool(
    name = "getEmployee",
    description = "Returns a single employee by employee ID."
)
public Employee getEmployee(
        @ToolParam(description = "Employee ID")
        Long employeeId) {

    return employeeService.getEmployee(employeeId);
}

Spring AI reads:

 

·      Tool name

·      Description

·      Method parameters

·      Parameter descriptions

·      Return type

 

and automatically converts them into an MCP Tool definition. There is no JSON schema to write manually.

 

Step 3: Register the Tool Bean

The only configuration required is a ToolCallbackProvider.

@Configuration
public class McpConfig {

    @Bean
    public ToolCallbackProvider employeeToolCallbackProvider(
            EmployeeTools employeeTools) {

        return MethodToolCallbackProvider.builder()
                .toolObjects(employeeTools)
                .build();
    }
}

   

That's it. Spring AI now scans every @Tool method inside EmployeeTools.

 

EmployeeTools.java

package com.sample.app.tools;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import com.sample.app.model.CountByGroup;
import com.sample.app.model.Employee;
import com.sample.app.model.EmployeeCount;
import com.sample.app.model.PagedResponse;
import com.sample.app.service.EmployeeService;

/**
 * Exposes all 15 employee management capabilities as MCP Tools via Spring AI's
 * {@link Tool @Tool} annotation.
 *
 * <p>
 * <b>How @Tool works:</b>
 * <ol>
 * <li>Spring AI scans this bean for {@code @Tool}-annotated methods at
 * startup</li>
 * <li>It generates JSON Schema from method signatures and {@code @ToolParam}
 * descriptions</li>
 * <li>The schema is served to AI clients via {@code tools/list}</li>
 * <li>When an AI calls a tool, Spring AI deserializes parameters and invokes
 * the method</li>
 * <li>The typed return value is serialized to JSON (once — no
 * double-encoding)</li>
 * </ol>
 *
 * <p>
 * <b>Error handling:</b> Exceptions thrown here bubble to Spring AI, which
 * converts them into MCP error responses. Do NOT use {@code @ControllerAdvice}
 * — it does not intercept MCP tool execution.
 *
 * <p>
 * <b>Return types:</b> Always return typed DTOs, never raw JSON strings. Spring
 * AI serializes the object exactly once, avoiding double-encoding.
 */
@Service
public class EmployeeTools {

        private static final Logger log = LoggerFactory.getLogger(EmployeeTools.class);

        private final EmployeeService employeeService;

        /**
         * Constructor injection.
         *
         * @param employeeService the business logic layer
         */
        public EmployeeTools(EmployeeService employeeService) {
                this.employeeService = employeeService;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 1 — getEmployees
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Retrieve a paginated list of all employees.
         */
        @Tool(name = "getEmployees", description = """
                        Retrieve a paginated list of all employees in the organization.
                        Pagination is zero-indexed: pageNumber=0 is the first page.

                        Returns: currentPage, pageSize, totalPages, totalElements, and the employee list.

                        Use this tool to browse the employee directory when no specific filter is needed.
                        """)
        public PagedResponse<Employee> getEmployees(
                        @ToolParam(description = "Zero-based page number (0 = first page). Default: 0.") int pageNumber,
                        @ToolParam(description = "Number of employees to return per page (1–100). Default: 10.") int pageSize) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployees called — page={}, size={}", pageNumber, pageSize);

                PagedResponse<Employee> result = employeeService.getEmployees(pageNumber, pageSize);

                log.info("[TOOL] getEmployees returned {} employees (total={}) in {}ms", result.getData().size(),
                                result.getTotalElements(), System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 2 — getEmployeesUnderManager
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get all direct reports of a given manager.
         */
        @Tool(name = "getEmployeesUnderManager", description = """
                        Get all employees who directly report to the specified manager.

                        Returns every employee whose managerId equals the given managerId.
                        Use getManagers() first to discover valid manager IDs.
                        """)
        public List<Employee> getEmployeesUnderManager(
                        @ToolParam(description = "The numeric ID of the manager (e.g., 8 for Vikram Rao).") Long managerId) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployeesUnderManager called — managerId={}", managerId);

                List<Employee> result = employeeService.getEmployeesUnderManager(managerId);

                log.info("[TOOL] getEmployeesUnderManager returned {} direct reports in {}ms", result.size(),
                                System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 3 — getEmployeesByCity
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get paginated employees in a given city.
         */
        @Tool(name = "getEmployeesByCity", description = """
                        Retrieve a paginated list of employees located in the specified city.
                        The city name comparison is case-insensitive (e.g., "bangalore" = "Bangalore").

                        Use getCities() to discover all available city names.
                        """)
        public PagedResponse<Employee> getEmployeesByCity(
                        @ToolParam(description = "City name to filter by (e.g., 'Bangalore', 'New York', 'Tokyo').") String city,
                        @ToolParam(description = "Zero-based page number. Default: 0.") int pageNumber,
                        @ToolParam(description = "Number of results per page (1–100). Default: 10.") int pageSize) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployeesByCity called — city={}, page={}, size={}", city, pageNumber, pageSize);

                PagedResponse<Employee> result = employeeService.getEmployeesByCity(city, pageNumber, pageSize);

                log.info("[TOOL] getEmployeesByCity returned {} employees in {}ms", result.getTotalElements(),
                                System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 4 — getEmployeesByCountry
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get paginated employees in a given country.
         */
        @Tool(name = "getEmployeesByCountry", description = """
                        Retrieve a paginated list of employees located in the specified country.
                        The country name comparison is case-insensitive.

                        Use getCountries() to discover all available country names.
                        Available countries: India, USA, Germany, Japan, Australia, UK, Canada, Singapore.
                        """)
        public PagedResponse<Employee> getEmployeesByCountry(
                        @ToolParam(description = "Country name to filter by (e.g., 'India', 'USA', 'Germany').") String country,
                        @ToolParam(description = "Zero-based page number. Default: 0.") int pageNumber,
                        @ToolParam(description = "Number of results per page (1–100). Default: 10.") int pageSize) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployeesByCountry called — country={}, page={}, size={}", country, pageNumber, pageSize);

                PagedResponse<Employee> result = employeeService.getEmployeesByCountry(country, pageNumber, pageSize);

                log.info("[TOOL] getEmployeesByCountry returned {} employees in {}ms", result.getTotalElements(),
                                System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 5 — searchEmployees
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Keyword search across employee fields.
         */
        @Tool(name = "searchEmployees", description = """
                        Search for employees by keyword across multiple fields:
                        firstName, lastName, fullName, designation, and department.

                        The search is case-insensitive and uses partial matching.
                        Examples:
                          - "senior"    → all Senior Engineers
                          - "architect" → all Architects
                          - "sharma"    → employees with "Sharma" in their name
                          - "bangalore" → does NOT work — use getEmployeesByCity for city search
                        """)
        public List<Employee> searchEmployees(
                        @ToolParam(description = "Search keyword (matches against name, designation, department).") String keyword) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] searchEmployees called — keyword='{}'", keyword);

                List<Employee> result = employeeService.searchEmployees(keyword);

                log.info("[TOOL] searchEmployees returned {} matches for '{}' in {}ms", result.size(), keyword,
                                System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 6 — getEmployee
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get a single employee by ID.
         */
        @Tool(name = "getEmployee", description = """
                        Retrieve the complete profile of a single employee by their numeric ID.
                        Returns all fields: id, name, email, phone, designation, department,
                        location, manager info, salary, joining date, and active status.

                        Use getEmployees() to discover valid employee IDs.
                        """)
        public Employee getEmployee(
                        @ToolParam(description = "The unique numeric employee ID (e.g., 42).") Long employeeId) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployee called — employeeId={}", employeeId);

                Employee result = employeeService.getEmployee(employeeId);

                log.info("[TOOL] getEmployee returned '{}' in {}ms", result.getFullName(), System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 7 — getManagers
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get all employees who manage at least one other employee.
         */
        @Tool(name = "getManagers", description = """
                        Returns all employees who have at least one direct report.
                        Includes managers at all levels: CEO, VP, Director, and Manager.

                        Useful before calling getEmployeesUnderManager() to know valid manager IDs.
                        """)
        public List<Employee> getManagers() {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getManagers called");

                List<Employee> result = employeeService.getManagers();

                log.info("[TOOL] getManagers returned {} managers in {}ms", result.size(), System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 8 — getDepartments
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get all unique department names.
         */
        @Tool(name = "getDepartments", description = """
                        Returns a sorted list of all unique department names in the organization.
                        Use these values as input for getEmployeesByDepartment() and countEmployeesByDepartment().
                        """)
        public List<String> getDepartments() {

                log.info("[TOOL] getDepartments called");
                List<String> result = employeeService.getDepartments();
                log.info("[TOOL] getDepartments returned {} departments", result.size());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 9 — getCountries
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get all unique country names.
         */
        @Tool(name = "getCountries", description = """
                        Returns a sorted list of all unique country names where the company has employees.
                        Use these values as input for getEmployeesByCountry() and countEmployeesByCountry().
                        """)
        public List<String> getCountries() {

                log.info("[TOOL] getCountries called");
                List<String> result = employeeService.getCountries();
                log.info("[TOOL] getCountries returned {} countries", result.size());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 10 — getCities
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get all unique city names.
         */
        @Tool(name = "getCities", description = """
                        Returns a sorted list of all unique city names where the company has offices.
                        Use these values as input for getEmployeesByCity().
                        """)
        public List<String> getCities() {

                log.info("[TOOL] getCities called");
                List<String> result = employeeService.getCities();
                log.info("[TOOL] getCities returned {} cities", result.size());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 11 — getEmployeesByDepartment
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get paginated employees in a department.
         */
        @Tool(name = "getEmployeesByDepartment", description = """
                        Retrieve a paginated list of employees in the specified department.
                        The department name comparison is case-insensitive.

                        Use getDepartments() to discover all available department names.
                        Available departments: Engineering, Finance, Sales, HR, Marketing,
                        Support, Operations, Legal, Security, Executive.
                        """)
        public PagedResponse<Employee> getEmployeesByDepartment(
                        @ToolParam(description = "Department name to filter by (e.g., 'Engineering', 'Sales').") String department,
                        @ToolParam(description = "Zero-based page number. Default: 0.") int pageNumber,
                        @ToolParam(description = "Number of results per page (1–100). Default: 10.") int pageSize) {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getEmployeesByDepartment called — dept={}, page={}, size={}", department, pageNumber,
                                pageSize);

                PagedResponse<Employee> result = employeeService.getEmployeesByDepartment(department, pageNumber, pageSize);

                log.info("[TOOL] getEmployeesByDepartment returned {} employees in {}ms", result.getTotalElements(),
                                System.currentTimeMillis() - start);
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 12 — countEmployees
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get total, active, and inactive employee counts.
         */
        @Tool(name = "countEmployees", description = """
                        Returns aggregate headcount statistics for the entire organization:
                          - totalEmployees   : total headcount
                          - activeEmployees  : currently active employees
                          - inactiveEmployees: employees who are inactive (on leave / terminated)
                        """)
        public EmployeeCount countEmployees() {

                log.info("[TOOL] countEmployees called");
                EmployeeCount result = employeeService.countEmployees();
                log.info("[TOOL] countEmployees — total={}, active={}, inactive={}", result.getTotalEmployees(),
                                result.getActiveEmployees(), result.getInactiveEmployees());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 13 — countEmployeesByCountry
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get employee count per country.
         */
        @Tool(name = "countEmployeesByCountry", description = """
                        Returns the number of employees per country, sorted by count descending.
                        Each entry contains: group (country name) and count.

                        Useful for understanding the geographic distribution of the workforce.
                        """)
        public List<CountByGroup> countEmployeesByCountry() {

                log.info("[TOOL] countEmployeesByCountry called");
                List<CountByGroup> result = employeeService.countEmployeesByCountry();
                log.info("[TOOL] countEmployeesByCountry returned {} country groups", result.size());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 14 — countEmployeesByDepartment
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get employee count per department.
         */
        @Tool(name = "countEmployeesByDepartment", description = """
                        Returns the number of employees per department, sorted by count descending.
                        Each entry contains: group (department name) and count.

                        Useful for understanding team sizes and organizational structure.
                        """)
        public List<CountByGroup> countEmployeesByDepartment() {

                log.info("[TOOL] countEmployeesByDepartment called");
                List<CountByGroup> result = employeeService.countEmployeesByDepartment();
                log.info("[TOOL] countEmployeesByDepartment returned {} department groups", result.size());
                return result;
        }

        // ═══════════════════════════════════════════════════════════════════════
        // Tool 15 — getOrganizationHierarchy
        // ═══════════════════════════════════════════════════════════════════════

        /**
         * MCP Tool: Get the complete org hierarchy as a nested tree.
         */
        @Tool(name = "getOrganizationHierarchy", description = """
                        Returns the full organization hierarchy as a nested JSON tree.
                        Starts at the CEO and recursively includes all levels:
                          CEO → VP → Director → Manager → Individual Contributors

                        Each node contains: id, name, designation, department, country, city,
                        and a directReports array with nested subtrees.

                        Warning: this returns the entire tree. For targeted queries,
                        use getEmployeesUnderManager() with a specific manager ID instead.
                        """)
        public Map<String, Object> getOrganizationHierarchy() {

                long start = System.currentTimeMillis();
                log.info("[TOOL] getOrganizationHierarchy called");

                Map<String, Object> result = employeeService.getOrganizationHierarchy();

                log.info("[TOOL] getOrganizationHierarchy built in {}ms", System.currentTimeMillis() - start);
                return result;
        }
}

   

McpConfig.java

package com.sample.app.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.sample.app.tools.EmployeeTools;

/**
 * MCP Server configuration.
 *
 * <p>
 * This class wires the three MCP primitives into the Spring AI
 * auto-configuration:
 *
 * <ol>
 * <li><b>Tools</b> — registered here via {@link ToolCallbackProvider}. Spring
 * AI introspects all {@code @Tool}-annotated methods, generates JSON Schema
 * from parameter types and {@code @ToolParam} descriptions, and serves them via
 * {@code tools/list}.</li>
 *
 * <li><b>Resources</b> — registered in
 * {@link com.sample.app.resources.EmployeeResourceRegistrar} as a
 * {@code List<McpStatelessServerFeatures.SyncResourceSpecification>} bean. The
 * autoconfigure flat-maps all such beans via {@code ObjectProvider}.</li>
 *
 * <li><b>Prompts</b> — registered in
 * {@link com.sample.app.prompts.EmployeePromptRegistrar} as a
 * {@code List<McpStatelessServerFeatures.SyncPromptSpecification>} bean. Same
 * flat-map mechanism.</li>
 * </ol>
 *
 * <p>
 * <b>STATELESS transport note:</b> Because
 * {@code spring.ai.mcp.server.protocol=STATELESS} is set in
 * {@code application.yml}, the server uses {@code McpStatelessSyncServer} (not
 * {@code McpSyncServer}). This requires the stateless variants of the
 * specification types:
 * {@code McpStatelessServerFeatures.SyncResourceSpecification} and
 * {@code McpStatelessServerFeatures.SyncPromptSpecification}.
 */
@Configuration
public class McpConfig {

        /**
         * Registers all {@link EmployeeTools} {@code @Tool}-annotated methods as MCP
         * tool callbacks.
         *
         * <p>
         * {@link MethodToolCallbackProvider} uses reflection to scan the bean for
         * {@code @Tool} methods and generates the JSON Schema descriptors that AI
         * clients use to understand how to call each tool.
         *
         * @param employeeTools the bean containing all 15 tool methods
         * @return the provider that the MCP auto-configuration consumes
         */
        @Bean
        public ToolCallbackProvider employeeToolCallbackProvider(EmployeeTools employeeTools) {
                return MethodToolCallbackProvider.builder().toolObjects(employeeTools).build();
        }
}

Step 11: Define main Application class.

 

App.java

package com.sample.app;

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

/**
 * Entry point for the Employee MCP Server.
 *
 * <p>
 * This application demonstrates how to build a complete MCP (Model Context
 * Protocol) server using Spring AI. It exposes employee management capabilities
 * through three MCP primitives:
 *
 * <ul>
 * <li><b>Tools</b> — 15 callable functions (e.g., getEmployees,
 * searchEmployees)</li>
 * <li><b>Resources</b> — 15 static data endpoints (e.g., employees://all)</li>
 * <li><b>Prompts</b> — 6 reusable prompt templates (e.g.,
 * employee-summary)</li>
 * </ul>
 *
 * <p>
 * Transport: Stateless HTTP (POST /mcp). No SSE sessions required.
 *
 * <p>
 * Data: ~100 in-memory employees initialized at startup. No database needed.
 *
 * @see com.sample.app.tools.EmployeeTools
 * @see com.sample.app.resources.EmployeeResourceRegistrar
 * @see com.sample.app.prompts.EmployeePromptRegistrar
 */
@SpringBootApplication
public class App {

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

   

Total project structure looks like below.

  


   

Build the Project

Navigate to the project root directory (the directory containing the pom.xml file) and execute the following Maven command to compile the source code, run the tests, and package the application into an executable JAR.

mvn clean package

   

After the build completes successfully, the packaged JAR file will be generated in the target directory.

 

Run the Application

Start the MCP server by executing the generated JAR file.

java -jar target/employee-mcp-1.0.0.jar

   

By default, the application starts on port 8080, making the MCP endpoint available at:

http://localhost:8080/mcp

 

Once the server is running, it is ready to accept requests from any MCP-compatible client, such as Claude Desktop, ChatGPT, Cursor, VS Code, or a custom AI agent.

 

For example, following curl command gets all the tools that are exposed by this MCP Server.

 

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list",
    "params": {}
  }' | jq .

$ curl -s -X POST http://localhost:8080/mcp \
>   -H "Content-Type: application/json" \
>   -H "Accept: application/json, text/event-stream" \
>   -d '{
>     "jsonrpc": "2.0",
>     "id": 2,
>     "method": "tools/list",
>     "params": {}
>   }' | jq .
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "countEmployeesByDepartment",
        "description": "Returns the number of employees per department, sorted by count descending.\nEach entry contains: group (department name) and count.\n\nUseful for understanding team sizes and organizational structure.\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployeesByCity",
        "description": "Retrieve a paginated list of employees located in the specified city.\nThe city name comparison is case-insensitive (e.g., \"bangalore\" = \"Bangalore\").\n\nUse getCities() to discover all available city names.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "City name to filter by (e.g., 'Bangalore', 'New York', 'Tokyo')."
            },
            "pageNumber": {
              "type": "integer",
              "description": "Zero-based page number. Default: 0."
            },
            "pageSize": {
              "type": "integer",
              "description": "Number of results per page (1–100). Default: 10."
            }
          },
          "required": [
            "city",
            "pageNumber",
            "pageSize"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployeesUnderManager",
        "description": "Get all employees who directly report to the specified manager.\n\nReturns every employee whose managerId equals the given managerId.\nUse getManagers() first to discover valid manager IDs.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "managerId": {
              "type": "integer",
              "description": "The numeric ID of the manager (e.g., 8 for Vikram Rao)."
            }
          },
          "required": [
            "managerId"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployees",
        "description": "Retrieve a paginated list of all employees in the organization.\nPagination is zero-indexed: pageNumber=0 is the first page.\n\nReturns: currentPage, pageSize, totalPages, totalElements, and the employee list.\n\nUse this tool to browse the employee directory when no specific filter is needed.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "pageNumber": {
              "type": "integer",
              "description": "Zero-based page number (0 = first page). Default: 0."
            },
            "pageSize": {
              "type": "integer",
              "description": "Number of employees to return per page (1–100). Default: 10."
            }
          },
          "required": [
            "pageNumber",
            "pageSize"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getOrganizationHierarchy",
        "description": "Returns the full organization hierarchy as a nested JSON tree.\nStarts at the CEO and recursively includes all levels:\n  CEO → VP → Director → Manager → Individual Contributors\n\nEach node contains: id, name, designation, department, country, city,\nand a directReports array with nested subtrees.\n\nWarning: this returns the entire tree. For targeted queries,\nuse getEmployeesUnderManager() with a specific manager ID instead.\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "countEmployeesByCountry",
        "description": "Returns the number of employees per country, sorted by count descending.\nEach entry contains: group (country name) and count.\n\nUseful for understanding the geographic distribution of the workforce.\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "countEmployees",
        "description": "Returns aggregate headcount statistics for the entire organization:\n  - totalEmployees   : total headcount\n  - activeEmployees  : currently active employees\n  - inactiveEmployees: employees who are inactive (on leave / terminated)\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "getManagers",
        "description": "Returns all employees who have at least one direct report.\nIncludes managers at all levels: CEO, VP, Director, and Manager.\n\nUseful before calling getEmployeesUnderManager() to know valid manager IDs.\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "getCountries",
        "description": "Returns a sorted list of all unique country names where the company has employees.\nUse these values as input for getEmployeesByCountry() and countEmployeesByCountry().\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployeesByDepartment",
        "description": "Retrieve a paginated list of employees in the specified department.\nThe department name comparison is case-insensitive.\n\nUse getDepartments() to discover all available department names.\nAvailable departments: Engineering, Finance, Sales, HR, Marketing,\nSupport, Operations, Legal, Security, Executive.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "department": {
              "type": "string",
              "description": "Department name to filter by (e.g., 'Engineering', 'Sales')."
            },
            "pageNumber": {
              "type": "integer",
              "description": "Zero-based page number. Default: 0."
            },
            "pageSize": {
              "type": "integer",
              "description": "Number of results per page (1–100). Default: 10."
            }
          },
          "required": [
            "department",
            "pageNumber",
            "pageSize"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "searchEmployees",
        "description": "Search for employees by keyword across multiple fields:\nfirstName, lastName, fullName, designation, and department.\n\nThe search is case-insensitive and uses partial matching.\nExamples:\n  - \"senior\"    → all Senior Engineers\n  - \"architect\" → all Architects\n  - \"sharma\"    → employees with \"Sharma\" in their name\n  - \"bangalore\" → does NOT work — use getEmployeesByCity for city search\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "keyword": {
              "type": "string",
              "description": "Search keyword (matches against name, designation, department)."
            }
          },
          "required": [
            "keyword"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployee",
        "description": "Retrieve the complete profile of a single employee by their numeric ID.\nReturns all fields: id, name, email, phone, designation, department,\nlocation, manager info, salary, joining date, and active status.\n\nUse getEmployees() to discover valid employee IDs.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "employeeId": {
              "type": "integer",
              "description": "The unique numeric employee ID (e.g., 42)."
            }
          },
          "required": [
            "employeeId"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getEmployeesByCountry",
        "description": "Retrieve a paginated list of employees located in the specified country.\nThe country name comparison is case-insensitive.\n\nUse getCountries() to discover all available country names.\nAvailable countries: India, USA, Germany, Japan, Australia, UK, Canada, Singapore.\n",
        "inputSchema": {
          "type": "object",
          "properties": {
            "country": {
              "type": "string",
              "description": "Country name to filter by (e.g., 'India', 'USA', 'Germany')."
            },
            "pageNumber": {
              "type": "integer",
              "description": "Zero-based page number. Default: 0."
            },
            "pageSize": {
              "type": "integer",
              "description": "Number of results per page (1–100). Default: 10."
            }
          },
          "required": [
            "country",
            "pageNumber",
            "pageSize"
          ],
          "additionalProperties": false
        }
      },
      {
        "name": "getCities",
        "description": "Returns a sorted list of all unique city names where the company has offices.\nUse these values as input for getEmployeesByCity().\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      },
      {
        "name": "getDepartments",
        "description": "Returns a sorted list of all unique department names in the organization.\nUse these values as input for getEmployeesByDepartment() and countEmployeesByDepartment().\n",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        }
      }
    ]
  }
}

   

In the next post, we'll take a deep dive into the MCP protocol by exploring each endpoint step by step. We'll use `curl` commands to interact with the server and understand how initialize, tools, resources, and prompts work in practice, along with the request and response payloads for each endpoint.

 

You can download this application from this link.

 

Previous                                                    Next                                                    Home

No comments:

Post a Comment