惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
I
InfoQ
Engineering at Meta
Engineering at Meta
D
DataBreaches.Net
L
LangChain Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Announcements
Recent Announcements
GbyAI
GbyAI
爱范儿
爱范儿
Microsoft Security Blog
Microsoft Security Blog
腾讯CDC
美团技术团队
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
雷峰网
雷峰网
M
MIT News - Artificial intelligence
D
Docker
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs
博客园 - 叶小钗

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building an MCP Server in Spring Boot
Pedro Santos · 2026-04-30 · via DEV Community

Building an MCP Server in Spring Boot (Step by Step)

In the previous post, I explained what MCP is and why it matters. Now let's build one. I'll take my payment-service, a regular Spring Boot app with PostgreSQL and Kafka, and add an MCP server to it in about 30 minutes.

By the end, the service will expose three tools that any AI agent can discover and call: getPaymentStatus, getRefundRate, and getFraudRiskScore.

Starting Point

My payment-service already has these Spring beans:

@Service
public class PaymentService {
    public Optional<Payment> findByTransactionId(String txId) { ... }
    public long count() { ... }
    public long countByStatus(PaymentStatus status) { ... }
    public List<Payment> findByStatusOrderByCreatedAtDesc(PaymentStatus status) { ... }
}

@Service
public class FraudValidationService {
    public String calculateFraudScore(double amount, String clientType, int hour,
                                      int orderCount, double successRate, int totalItems) { ... }
}

Enter fullscreen mode Exit fullscreen mode

These are existing business logic methods with real database queries. The goal is to expose them via MCP without changing their implementation.

Step 1: Add the Dependency

implementation 'io.modelcontextprotocol.sdk:mcp:0.9.0'

Enter fullscreen mode Exit fullscreen mode

That's the only new dependency. The MCP SDK is lightweight and has no transitive dependencies that conflict with Spring Boot.

Step 2: Set Up the SSE Transport

MCP needs an HTTP transport for communication. The SDK provides an SSE-based transport that works as a servlet:

@Configuration
public class PaymentMcpConfig {

    @Bean
    public HttpServletSseServerTransportProvider mcpTransport() {
        return HttpServletSseServerTransportProvider.builder()
            .objectMapper(new ObjectMapper())
            .messageEndpoint("/mcp/message")
            .build();
    }

    @Bean
    public ServletRegistrationBean<HttpServletSseServerTransportProvider> mcpServlet(
            HttpServletSseServerTransportProvider transport) {
        return new ServletRegistrationBean<>(transport, "/sse", "/mcp/message");
    }
}

Enter fullscreen mode Exit fullscreen mode

Two endpoints are registered. /sse is the SSE connection endpoint where clients connect. /mcp/message is where JSON-RPC messages are sent. The objectMapper handles serialization.

Step 3: Define Your Tools

Each tool has four components: a name, a description (this is what the LLM reads to decide when to use it), a JSON schema for parameters, and a handler function.

Here's the payment status tool:

private SyncToolSpecification getPaymentStatus(PaymentService paymentService) {
    return tool(
        "getPaymentStatus",
        "Returns the current payment status for a given transaction. " +
        "Use to verify whether a payment was processed, pending, or refunded.",
        """
        {
          "type": "object",
          "properties": {
            "transactionId": {
              "type": "string",
              "description": "Transaction ID associated with the saga"
            }
          },
          "required": ["transactionId"]
        }
        """,
        args -> {
            String txId = (String) args.get("transactionId");
            return paymentService.findByTransactionId(txId)
                .map(p -> "status=" + p.getStatus()
                    + " | totalAmount=" + p.getTotalAmount()
                    + " | totalItems=" + p.getTotalItems())
                .orElse("No payment found for transactionId=" + txId);
        }
    );
}

Enter fullscreen mode Exit fullscreen mode

The description matters more than you'd expect. The LLM uses it to decide when to call this tool. A vague description like "gets payment info" leads to the agent calling it at wrong times. A precise description like "returns payment status for a given transaction, use to verify whether processed, pending, or refunded" gives the LLM the context it needs.

The handler is a Function<Map<String, Object>, String>. It receives the arguments as a map, calls your existing business logic, and returns a string. The return value goes back to the LLM as context for generating its response.

Step 4: Build the MCP Server

Wire everything together:

@Bean
public McpSyncServer mcpServer(
        HttpServletSseServerTransportProvider transport,
        PaymentService paymentService,
        FraudValidationService fraudService) {

    return McpServer.sync(transport)
        .serverInfo("payment-mcp", "1.0.0")
        .capabilities(ServerCapabilities.builder().tools(true).build())
        .tools(
            getPaymentStatus(paymentService),
            getRefundRate(paymentService),
            getFraudRiskScore(fraudService)
        )
        .build();
}

Enter fullscreen mode Exit fullscreen mode

The serverInfo is what the client sees when it connects. The capabilities declaration tells the client this server supports tools. The .tools(...) call registers all your tool specifications.

A Helper to Reduce Boilerplate

I use a small helper method to avoid repeating the SyncToolSpecification construction:

private SyncToolSpecification tool(String name,
                                   String description,
                                   String schema,
                                   Function<Map<String, Object>, String> handler) {
    return new SyncToolSpecification(
        new McpSchema.Tool(name, description, schema),
        (exchange, args) -> success(handler.apply(args))
    );
}

private CallToolResult success(String text) {
    return CallToolResult.builder()
        .content(List.of(new TextContent(text)))
        .isError(false)
        .build();
}

Enter fullscreen mode Exit fullscreen mode

Every tool follows the same pattern: receive args, call business logic, return text. The helper keeps each tool definition focused on its own logic.

The Full Config for All 4 Services

I repeated this pattern for each microservice. The tools map directly to existing service methods:

order-service (MongoDB):

.tools(
    getOrderById(orderRepository),
    getLastEventByOrder(eventRepository),
    listRecentEvents(eventRepository)
)

Enter fullscreen mode Exit fullscreen mode

inventory-service (PostgreSQL):

.tools(
    getStockByProduct(inventoryService),
    getLowStockAlert(inventoryService),
    checkReservationExists(orderInventoryRepository)
)

Enter fullscreen mode Exit fullscreen mode

product-validation-service (PostgreSQL):

.tools(
    checkProductExists(productValidationService),
    checkValidationExists(productValidationService),
    listCatalog(productValidationService)
)

Enter fullscreen mode Exit fullscreen mode

Each config class follows the same structure. Transport bean, servlet registration, server bean with tools. Copy the pattern, change the tools.

Writing Good Tool Descriptions

After building all 12 tools, I noticed a pattern in which descriptions work well with LLMs and which ones cause problems.

Bad description: "Gets stock." The LLM doesn't know when to call this vs the payment tool.

Good description: "Returns the current available stock quantity for a given product code. Use this tool to check whether a product has sufficient inventory before processing a saga order."

The trick is to include two things: what the tool returns and when to use it. The "use this tool to..." part gives the LLM decision criteria.

For parameters, be explicit about valid values:

{
  "productCode": {
    "type": "string",
    "description": "Product code: COMIC_BOOKS, BOOKS, MOVIES, MUSIC"
  }
}

Enter fullscreen mode Exit fullscreen mode

Listing the valid values in the description prevents the LLM from inventing codes like "COMICS" or "BOOK".

What's Next

The servers are running. In the next post, I'll show the client side: how to connect to multiple MCP servers from a single LangChain4j agent and let the LLM pick the right tools at runtime.

The repo: github.com/pedrop3/saga-orchestration