Building an MCP Server in Java: Connecting Your Spring Boot App to AI Agents

spring boot MCP server


AI agents can now use tools. The Model Context Protocol (MCP) is how you let them safely use **yours**. Here is how to turn an ordinary Spring Boot app into an MCP server — so Claude and other agents can call your code — using Spring Boot 4 and Spring AI. No LLM, no API key, no database required.


What you’ll learn:


What the Model Context Protocol (MCP) is and why it matters, how to build an MCP **server** in Java with the Spring AI MCP server starter, how to expose your service methods as **tools** with the `@McpTool` annotation, the two transports (**stdio** and **Streamable HTTP**) and when to use each, how to test your server with the **MCP Inspector**, and how to connect **real AI agents** — Claude Desktop and Claude Code — so they can read and change your application’s data. The complete working example is on GitHub: tucanoo/spring-mcp-server-tutorial.


A year ago, “AI in your app” meant calling a language model and reading back text. That has changed. Modern AI assistants — Claude, Cursor, GitHub Copilot, and others — are now agents: they can call tools to look things up and take actions on your behalf. An agent that can only talk is a chatbot. An agent that can call your order system, your CRM, or your internal API is something far more useful.

The obvious question for a Java developer is: how do I let an agent call my code? You could build a custom integration for each assistant, but that does not scale — every new agent would need its own bespoke connector. This is exactly the problem the Model Context Protocol solves.

In this tutorial we will build a small but complete MCP server for a fictional online store. It exposes the store’s operations — searching products, checking stock, looking up and creating orders — as tools that any MCP-capable agent can call. By the end, you will watch Claude search your catalogue, create an order, and mark it as shipped, all by calling your Spring Boot code.

And here is the pleasant surprise: an MCP server needs no AI of its own. There is no language model to host, no API key to buy, and in our case no database to run. The server is just Spring beans. The intelligence lives in whichever agent connects to it.


What is MCP, in plain English?

The Model Context Protocol (MCP) is an open standard, created by Anthropic, for connecting AI applications to external tools and data. Think of it as a universal adapter — people often call it “the USB-C of AI integrations.” You build one MCP server for your app, and any MCP-capable agent can use it. No more one-off integrations per assistant.

There are three roles to know:

  • Host — the AI application the user interacts with (Claude Desktop, Claude Code, an IDE). It contains the language model.
  • Client — a connector inside the host. The host runs one client per server it talks to.
  • Server — a program that exposes capabilities. That is what we are building.

An MCP server can expose three kinds of things:

  • Tools — functions the agent can call to do something (search products, create an order). This is what most servers focus on, and most of this tutorial.
  • Resources — read-only data the agent can load by a URI (a document, a record).
  • Prompts — reusable prompt templates the host can offer to the user.

Under the hood, MCP is built on JSON-RPC 2.0. When a host connects, the two sides perform a short handshake: they agree a protocol version, the server announces its capabilities, and the host asks for the list of tools (tools/list). When the agent decides to use a tool, the host sends a tools/call request with the arguments. The good news is that Spring AI handles all of that wire-level detail for you — you write annotated Java methods, and the framework does the rest.


Two transports: stdio and Streamable HTTP

MCP defines two ways for a host and server to talk, and we will use both.

  • stdio — the host launches your server as a subprocess and talks to it over standard input/output. There is no network involved. This is the simplest option and the default for local desktop tools like Claude Desktop. The catch: because the protocol travels over stdout, your server must not print anything else there (more on that later).
  • Streamable HTTP — your server is a long-running web service that listens on a URL (for example http://localhost:8080/mcp). Many agents can connect to the same server, and you can deploy it like any other web app. This is the modern HTTP transport. (An older “HTTP+SSE” transport exists but is now deprecated — you do not need it.)

A simple rule of thumb: stdio for a local, single-user tool; Streamable HTTP for a shared or deployed service. We will start with stdio (and Claude Desktop), then graduate the same server to HTTP (and Claude Code).

The stack, and why each piece

PieceChoice
FrameworkSpring Boot 4.0.6
MCP supportSpring AI 2.0.0-RC1
MCP server starterspring-ai-starter-mcp-server-webmvc
BoilerplateLombok
TestingMCP Inspector (npx)
AgentsClaude Desktop (stdio) and Claude Code (HTTP)

🟨 A note on versions (please read). Spring AI’s 2.0.x line is the one that targets Spring Boot 4. At the time of writing the latest published version is 2.0.0-RC1 — RC meaning release candidate, a pre-GA build. It is on Maven Central and works well, but the API may still change before the final 2.0.0 release. For production, pin the exact version (we do, below) and plan to move to 2.0.0 GA when it ships. The older, fully stable Spring AI 1.1.x line targets Spring Boot 3, not 4.

💡 No model, no key, no database. This server does not call a language model, so there is no OpenAI (or other) API key, and no cost per request. Our data lives in memory and is seeded at startup. The whole project is plain Spring beans plus a handful of annotations.


Prerequisites

  • Java 17 or newer. This sample is built and tested on Java 25.
  • IntelliJ IDEA (Community is fine) — used for the bootstrap screenshots.
  • Node.js — only to run the MCP Inspector via npx.
  • An MCP-capable agent to connect to: Claude Desktop (for the stdio demo) and/or Claude Code (for the HTTP demo). Both are optional, but they make the payoff real.
  • About 30–45 minutes.

Creating the project

We start from start.spring.io (the Spring Initializr), built into IntelliJ IDEA under File → New → Project → Spring Boot.

Fill in the project details: a Gradle (Groovy) project, language Java, Spring Boot 4.0.6, group com.tucanoo, artifact product-mcp-server, and JDK 25.

intellij spring boot mcp setup

On the next screen, add just two dependencies:

  • Model Context Protocol Server (under AI) — the MCP server support.
  • Lombok — to cut boilerplate on our data classes.
intellij spring boot mcp server dependencies

Click Create. Initializr also adds Spring Boot DevTools (handy live-reload while you develop) — keep it.

One small change after generation: Initializr adds the spring-ai-starter-mcp-server starter, which only speaks stdio. We want a single starter that serves both transports, so we will switch it to the WebMVC variant in the build file next. (This is exactly what Spring’s own MCP examples do — the WebMVC starter can run stdio too.)


The build file

Here is the complete build.gradle, with the two edits we make to the generated file called out in the comments:

plugins {
  id 'java'
  id 'org.springframework.boot' version '4.0.6'
  id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.tucanoo'
version = '0.0.1-SNAPSHOT'
description = 'product-mcp-server'
java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(25)
  }
}
repositories {
  mavenCentral()
}
ext {
  // 2.0.x is the Spring AI line that targets Spring Boot 4. Latest is 2.0.0-RC1 (pre-GA);
  // pin it explicitly and move to 2.0.0 GA when it ships.
  set('springAiVersion', "2.0.0-RC1")
}
dependencies {
  // WebMVC MCP server starter: serves BOTH stdio (JVM args) and Streamable HTTP — one starter, no swap.
  implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
  compileOnly 'org.projectlombok:lombok'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testCompileOnly 'org.projectlombok:lombok'
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
  testAnnotationProcessor 'org.projectlombok:lombok'
}
dependencyManagement {
  imports {
    mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
  }
}
tasks.named('test') {
  useJUnitPlatform()
}

Two things worth pointing out:

  1. The Spring AI version is pinned via the BOM (spring-ai-bom) to 2.0.0-RC1. The starter line carries no version of its own — the BOM supplies it. This makes upgrading later a one-line change.
  2. We swapped the starter to -webmvc. That single dependency pulls in the MCP Java SDK and the Spring AI auto-configuration for both stdio and Streamable HTTP. We choose which one runs with a Spring profile.

Run ./gradlew build and everything resolves from Maven Central — no special repositories needed.

The app you’re exposing

Before any MCP code, let’s build the ordinary Spring Boot app: a tiny product catalogue and order system. This is the “real app” — the kind of service layer you already have. Nothing here knows anything about AI.

The domain classes use Lombok to stay short. @Data generates the getters, setters, equals, hashCode and toString; @Builder gives us a fluent builder:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    private Long id;
    private String sku;
    private String name;
    private String description;
    private String category;
    private BigDecimal price;
    private int stock;
}

Order, OrderLine and an OrderStatus enum (NEW, PAID, SHIPPED, CANCELLED) follow the same pattern. (These are mutable POJOs because we update an order’s status; if you prefer immutability for some of them, Java records work just as well.)

The catalogue is a normal @Service holding products in a map, seeded at startup:

@Service
public class CatalogService {
    private final Map<Long, Product> products = new LinkedHashMap<>();
    private final AtomicLong sequence = new AtomicLong();
    @PostConstruct
    void seed() {
        add("SKU-1001", "Aurora Wireless Headphones", "Over-ear Bluetooth headphones...", "Audio", "199.99", 42);
        add("SKU-1002", "Pulse Wireless Earbuds", "Compact in-ear buds...", "Audio", "89.50", 0);
        add("SKU-1004", "Glide Mechanical Keyboard", "Hot-swappable 75% keyboard...", "Accessories", "129.99", 27);
        // ...a handful more
    }
    public List<Product> search(String query) {
        if (query == null || query.isBlank()) {
            return new ArrayList<>(products.values());
        }
        String q = query.toLowerCase(Locale.ROOT);
        return products.values().stream()
                .filter(p -> p.getName().toLowerCase(Locale.ROOT).contains(q)
                        || p.getDescription().toLowerCase(Locale.ROOT).contains(q)
                        || p.getCategory().toLowerCase(Locale.ROOT).contains(q)
                        || p.getSku().toLowerCase(Locale.ROOT).contains(q))
                .toList();
    }
    public Optional<Product> findBySku(String sku) { /* ... */ }
}

OrderService is similar, but it has write methods too. The public create validates its inputs and then delegates to a private helper that does the real work: it checks the product exists, checks there is enough stock, and decrements it — a real, observable side effect, which is what makes the “an agent changed my data” demo convincing:

public Order create(String customerName, String sku, int quantity) {
    // validate inputs, then delegate to the private helper below
    if (customerName == null || customerName.isBlank()) {
        throw new IllegalArgumentException("customerName is required");
    }
    if (quantity < 1) {
        throw new IllegalArgumentException("quantity must be at least 1");
    }
    return createInternal(customerName, sku, quantity, OrderStatus.NEW);
}
private Order createInternal(String customerName, String sku, int quantity, OrderStatus status) {
    Product product = catalog.findBySku(sku)
            .orElseThrow(() -> new IllegalArgumentException("No product with SKU " + sku));
    if (product.getStock() < quantity) {
        throw new IllegalArgumentException(
                "Insufficient stock for " + sku + ": " + product.getStock() + " available, " + quantity + " requested");
    }
    product.setStock(product.getStock() - quantity);
    // ...build the OrderLine and Order, store it, return it
}

(createInternal is also reused at startup to seed a few orders.)

That is the entire application. Now we make it callable by an AI agent.


Defining tools with @McpTool

This is the only MCP-specific code in the project, and it is delightfully thin. A tool is just a Spring bean method annotated with @McpTool. Spring AI scans for these on startup, generates a JSON schema from the method’s parameters, and advertises them to any connected agent.

Here are the product tools:

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
@Component
@RequiredArgsConstructor
public class ProductTools {
    private final CatalogService catalog;
    @McpTool(name = "search_products",
            description = "Search the product catalog by keyword. Matches product name, description, "
                    + "category or SKU. Returns matching products with their price and current stock.",
            annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false))
    public List<Product> searchProducts(
            @McpToolParam(description = "Keyword to search for, e.g. 'headphones' or 'keyboard'", required = true)
            String query) {
        return catalog.search(query);
    }
    @McpTool(name = "get_product",
            description = "Get the full details of a single product by its SKU.",
            annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false))
    public Product getProduct(
            @McpToolParam(description = "The product SKU, e.g. 'SKU-1001'", required = true)
            String sku) {
        return catalog.findBySku(sku)
                .orElseThrow(() -> new IllegalArgumentException("No product with SKU " + sku));
    }
    @McpTool(name = "check_stock",
            description = "Check how many units of a product are currently in stock, by SKU.",
            annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false))
    public StockInfo checkStock(
            @McpToolParam(description = "The product SKU to check, e.g. 'SKU-1002'", required = true)
            String sku) {
        Product product = catalog.findBySku(sku)
                .orElseThrow(() -> new IllegalArgumentException("No product with SKU " + sku));
        return new StockInfo(product.getSku(), product.getName(), product.getStock(), product.getStock() > 0);
    }
}

A few things to notice:

  • The annotations live in org.springframework.ai.mcp.annotation. (Older community examples used a different package — make sure your imports match the ones above for Spring AI 2.0.)
  • description is not decoration — it is the API. The agent reads it to decide when and how to call your tool. Write descriptions as if for a junior colleague: say what the tool does and give an example of each parameter. The same goes for @McpToolParam.
  • The return type can be any object (a Product, a List<Product>, a small record like StockInfo). Spring AI serialises it to JSON for the agent.
  • The hints matter. readOnlyHint = true tells the host this tool only reads data, so it is safe. We will see the effect of that in a moment.

The order tools follow the same shape, but two of them change state — and we mark them accordingly:

@McpTool(name = "create_order",
        description = "Create a new order for a customer: a quantity of a single product, by SKU. "
                + "This reduces the product's stock and returns the created order.",
        annotations = @McpTool.McpAnnotations(readOnlyHint = false, destructiveHint = false, idempotentHint = false))
public Order createOrder(
        @McpToolParam(description = "Customer full name, e.g. 'Jane Doe'", required = true) String customerName,
        @McpToolParam(description = "Product SKU to order, e.g. 'SKU-1001'", required = true) String sku,
        @McpToolParam(description = "Quantity to order (1 or more)", required = true) int quantity) {
    return orders.create(customerName, sku, quantity);
}
@McpTool(name = "update_order_status",
        description = "Update the status of an existing order. Valid statuses: NEW, PAID, SHIPPED, CANCELLED.",
        annotations = @McpTool.McpAnnotations(readOnlyHint = false, destructiveHint = false, idempotentHint = true))
public Order updateOrderStatus(
        @McpToolParam(description = "The order ID to update", required = true) long orderId,
        @McpToolParam(description = "New status: NEW, PAID, SHIPPED or CANCELLED", required = true) String status) {
    return orders.updateStatus(orderId, OrderStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)));
}

Both writes set readOnlyHint = false. We keep destructiveHint = false because they create or update data but do not irreversibly delete anything, and we mark update_order_status as idempotentHint = true — setting the same status twice has the same effect. Hosts use these hints to decide how cautious to be, as we will see in Claude Desktop in a moment.

In total our server exposes seven tools: search_products, get_product, check_stock, list_orders, get_order (read), plus create_order and update_order_status (write).

There is no registration code to write. Spring AI’s annotation scanner finds these @Component beans and registers their @McpTool methods automatically. You do not need a ToolCallbackProvider bean or any wiring.

A quick word on resources

Tools are actions. The other primitive you will meet is a resource — read-only data addressed by a URI. One annotation is enough to show the idea:

@McpResource(uri = "catalog://products/{sku}",
        name = "Product details",
        description = "Human-readable details for a product, addressed by SKU")
public String product(String sku) {
    return catalog.findBySku(sku)
            .map(p -> "%s — %s\nPrice: $%s\nIn stock: %d".formatted(
                    p.getSku(), p.getName(), p.getPrice(), p.getStock()))
            .orElse("No product with SKU " + sku);
}

The {sku} in the URI template is bound to the method parameter. (The snippet above is simplified — the version in the repo returns a few more fields.) For the rest of this tutorial we will focus on tools, since that is what drives the demo.


Configuration: one server, two profiles

We use three small YAML files. The base application.yaml sets the server’s identity and turns on the capabilities:

spring:
  application:
    name: product-mcp-server
  ai:
    mcp:
      server:
        name: product-mcp-server
        version: 0.0.1
        type: SYNC          # blocking, servlet-style server (matches the WebMVC starter)
        capabilities:
          tool: true
          resource: true
          prompt: true

The stdio profile (application-stdio.yaml) is where the most important gotcha lives. With stdio, the JSON-RPC protocol travels over standard output — so anything else printed there (the Spring banner, a log line) will corrupt the stream and the agent will fail to connect. We therefore turn the web server off, turn the banner off, and blank the console log pattern (logs go to a file instead):

spring:
  main:
    web-application-type: none   # no Tomcat for stdio
    banner-mode: off
  ai:
    mcp:
      server:
        stdio: true
logging:
  pattern:
    console:                     # empty on purpose — keeps stdout clean for JSON-RPC
  file:
    name: product-mcp-server.log

The http profile (application-http.yaml) selects the Streamable HTTP transport:

spring:
  ai:
    mcp:
      server:
        protocol: STREAMABLE
server:
  port: 8080

That is it. The same code, the same seven tools, two transports — chosen at run time with --spring.profiles.active=stdio or =http.


Testing with the MCP Inspector

Before wiring up a real agent, test the server with the official MCP Inspector. It connects to your server exactly as a host would and lets you list and call tools by hand — so you debug the server, not the agent’s interpretation of it.

First build the jar:

./gradlew build

Then list the tools over stdio. We pass the profile as an environment variable so it reaches the child process the Inspector launches:

SPRING_PROFILES_ACTIVE=stdio npx @modelcontextprotocol/inspector --cli \
  java -jar build/libs/product-mcp-server-0.0.1-SNAPSHOT.jar --method tools/list

You will see all seven tools with their generated JSON schemas. Now call one — let’s check the earbuds, which we seeded as out of stock:

SPRING_PROFILES_ACTIVE=stdio npx @modelcontextprotocol/inspector --cli \
  java -jar build/libs/product-mcp-server-0.0.1-SNAPSHOT.jar \
  --method tools/call --tool-name check_stock --tool-arg sku=SKU-1002
{ "content": [ { "type": "text",
  "text": "{\"sku\":\"SKU-1002\",\"name\":\"Pulse Wireless Earbuds\",\"unitsInStock\":0,\"available\":false}" } ],
  "isError": false }

Drop the --cli flag to launch the Inspector’s web UI instead — it prints a pre-filled URL with a session token in the terminal, which you open in your browser to click through tools visually.


Connecting Claude Desktop (stdio)

Now the fun part: a real agent. Claude Desktop launches local MCP servers as subprocesses over stdio, so this is the most direct path.

Open Claude Desktop’s config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add a product-store server. Use an absolute path to your jar, and pass the stdio profile:

{
  "mcpServers": {
    "product-store": {
      "command": "java",
      "args": [
        "-jar",
        "/ABSOLUTE/PATH/TO/product-mcp-server/build/libs/product-mcp-server-0.0.1-SNAPSHOT.jar",
        "--spring.profiles.active=stdio"
      ]
    }
  }
}

Then fully quit and reopen Claude Desktop (not just close the window — it only reads the config on a fresh start).

If you open the connector’s settings, you can see Claude has grouped our tools by their hints — five read-only tools and two write/delete tools, each set to require your approval. This is the payoff of those readOnlyHint flags: the host knows which tools are safe and which change data.

claude desktop mcp connectors

Now just ask. A read first:

Spring example MCP Search

Claude calls search_products, gets your catalogue back, and answers from it. Ask “Is SKU-1002 in stock?” and it calls check_stock and reports the earbuds are out of stock:

Spring MCP SKU Search

Then a write — the one that proves the point:

Create an order for 2 of SKU-1001 for customer Jane Doe, then mark that order as shipped.

spring mcp claude order create and ship

Claude calls create_order (which checks stock, decrements it, and returns the new order) and then update_order_status to ship it. Before each write, Claude Desktop asks you to approve the call — a human-in-the-loop checkpoint that comes for free.

A realistic note on agents. Agents do not always chain multi-step requests perfectly. Now and then a model will create the order and then hesitate on the follow-up step. If that happens, just ask again, or split it into two prompts (“Create the order” then “Mark order 1006 as shipped”). Clear, single-purpose instructions and good tool descriptions make this far more reliable — which is itself a useful lesson in designing tools for agents.


The same server, remotely — Streamable HTTP

stdio is perfect for a local tool, but a real service usually needs to be shared: many users, many agents, deployed somewhere. That is what the Streamable HTTP transport is for, and we already have everything we need — just run the other profile:

java -jar build/libs/product-mcp-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=http

The server now listens at http://localhost:8080/mcp. (If port 8080 is busy on your machine, add --server.port=8081.) Unlike stdio, this is a normal web server — it stays running, serves many clients at once, and you can put it behind a load balancer or deploy it to the cloud. Logging is unrestricted here, because the protocol travels over HTTP, not stdout.

You can verify it the same way as before, pointing the Inspector at the URL:

npx @modelcontextprotocol/inspector --cli http://localhost:8080/mcp --transport http --method tools/list

Because the HTTP server is long-lived, state persists across calls: create an order, and a later get_order finds it; check stock before and after, and you will see it drop. (With stdio, each Inspector command launched a fresh process, so each one started from the seeded data again.)


Connecting Claude Code over HTTP

Claude Code — Anthropic’s command-line agent — connects to HTTP MCP servers directly, with no public exposure needed. With the server running, register it:

claude mcp add --transport http product-store http://localhost:8080/mcp

Inside Claude Code, run /mcp to confirm the connection. You will see the server listed as connected, with its seven tools:

claude mcp list connectors

Now ask it the same kinds of questions. A read:

claude cli mcp query

And a write, over the network this time:

Same tools, same Spring beans — now driven by a different agent over a different transport. That is the whole promise of MCP: build the server once, and any compliant host can use it.


Taking it to production

This tutorial keeps things local and simple, but here is the honest shape of what “real” looks like.

  • Remote hosts. Claude Desktop can connect to a remote HTTP server too, via its custom connectors feature — you paste the server’s URL. One thing to know: a custom connector connects from Anthropic’s cloud, not your laptop, so the server must be reachable on the public internet. For a quick demo you can expose a local server with a tunnel (such as cloudflared or ngrok); for real use, deploy it.
  • Security is not optional once tools can act. A tool can take actions, so treat every call as untrusted input: validate arguments, scope what you expose, and prefer read-only tools unless a write is truly needed. The readOnlyHint / destructiveHint flags help hosts present sensible approval prompts, and most hosts keep a human in the loop for writes — but do not rely on that alone.
  • Authentication for remote servers. The MCP spec defines an OAuth 2.1 based authorization flow for HTTP servers (your server acts as an OAuth resource server validating bearer tokens). That is well beyond a first server, but it is the direction to follow when you expose anything sensitive. For a local or internal demo, an unauthenticated server is fine.
  • Operations. Because this is an ordinary Spring Boot web app in HTTP mode, the usual tools apply — add Spring Boot Actuator for health and metrics, log and monitor as normal, and scale it like any service.

Troubleshooting

  • The agent can’t connect over stdio / it “hangs.” Almost always stdout pollution. Make sure the stdio profile sets spring.main.web-application-type=none, spring.main.banner-mode=off, and an empty logging.pattern.console. Anything printed to stdout breaks the JSON-RPC stream.
  • Claude Desktop doesn’t see your server. Use an absolute path to the jar, and fully quit and restart the app (not just close the window). Check the logs at ~/Library/Logs/Claude/mcp*.log.
  • Port 8080 was already in use in HTTP mode. Something else is on 8080 — run with --server.port=8081 and point your client at that port.
  • Wrong Spring AI version. Spring AI 1.x targets Spring Boot 3 and will not line up with Boot 4. Use the 2.0.x line (here, 2.0.0-RC1) via the BOM.
  • Annotation import errors. The annotations are in org.springframework.ai.mcp.annotation. Older blog posts may show a different package — trust your IDE against the version in your build.gradle.
  • The Inspector shows a blank page. It prints a URL with a session token in the terminal — open that URL, not the bare address (localhost:6274, the Inspector’s default UI port).

Conclusion

You have built a real MCP server in Java. Starting from an ordinary Spring Boot service, you exposed its operations as tools with a single annotation, ran it over two transports, tested it with the MCP Inspector, and watched two different AI agents — Claude Desktop and Claude Code — search your catalogue, create orders, and ship them by calling your code. No language model, no API key, and no database in sight.

This is the other half of building with AI. Our RAG tutorial showed how to let your app consume a language model; this one shows how to let agents use your app. Together they cover both directions of the modern AI integration story — entirely in Java, on the latest Spring stack.

The complete, working example is on GitHub: tucanoo/spring-mcp-server-tutorial. Clone it, point Claude at it, and watch an agent put your code to work.


Building AI features into a Java product and want a hand shipping them? Tucanoo helps teams design and build production software, including practical AI integrations on the Spring stack. Get in touch.

David Brown founded Tucanoo Solutions in 2011 and has written software in a professional capacity for over 30 years, back to the Java/J2EE days of Sun Microsystems. He builds cloud and web applications for clients in legal tech, government and beyond, including several years consulting to the United Nations on customs and transport systems. He writes these tutorials himself, with the full code on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *