Friday, April 18, 2025

How to Build MCP Server with Java

This guide walks you through building a streamlined Model Context Protocol (MCP) server using only the core Java SDK, no external frameworks like Spring required. We’ll set up the MCP Java dependency, implement a server, configure default transport implementations, and test everything with the MCP Inspector tool. The example revolves around a simple application that serves a list of JavaOne conference presentations. Let’s dive into the code.

Setting Up the Project

Create a new Maven project in IntelliJ (or your preferred IDE) with JDK 24. Name it JavaOneMCP for clarity. Update the pom.xml to include the MCP Java SDK and SLF4J for logging.

4.0.0 dev.danvega JavaOneMCP 1.0-SNAPSHOT

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.modelcontextprotocol</groupId>
            <artifactId>mcp-sdk-java</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.9</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.modelcontextprotocol</groupId>
        <artifactId>mcp-sdk-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.9</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.5.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>dev.danvega.Application</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>


 The MCP SDK (version 0.9.0) provides the core functionality, while SLF4J handles logging. The Maven Shade Plugin ensures the JAR file includes all dependencies and specifies the main class.

Defining the Data Model

Create a Presentation record to represent JavaOne presentations with a title, URL, and year.

package dev.danvega; public record Presentation(String title, String url, int year) { }

Next, create a PresentationTools class to manage a collection of presentations and provide access methods.

package dev.danvega; import java.util.ArrayList; import java.util.List; public class PresentationTools { private final List<Presentation> presentations = new ArrayList<>(); public PresentationTools() { presentations.addAll(List.of( new Presentation("Concerto for Java and AI", "https://youtube.com/javaone2025/keynote1", 2025), new Presentation("Stream Gatherers", "https://youtube.com/javaone2025/streams", 2025), new Presentation("AI 2.0", "https://youtube.com/javaone2025/ai2", 2025), new Presentation("Sequence Collections", "https://youtube.com/javaone2025/collections", 2025) )); } public List<Presentation> getPresentations() { return presentations; } public List<Presentation> getPresentationsByYear(int year) { return presentations.stream() .filter(p -> p.year() == year) .toList(); } }

This class initializes a list of presentations and offers methods to retrieve all presentations or filter by year. The data is hardcoded for simplicity, but you could extend it to pull from a database or API.

Building the MCP Server

Now, implement the MCP server in the main application class. This involves setting up the transport, defining tool specifications, and configuring the server.


package dev.danvega; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.mcp.schema.MCPSchema; import io.modelcontextprotocol.mcp.server.MCPServer; import io.modelcontextprotocol.mcp.server.MCPServerFeatures; import io.modelcontextprotocol.mcp.transport.STDIOServerTransportProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; public class Application { private static final Logger logger = LoggerFactory.getLogger(Application.class); private static final PresentationTools presentationTools = new PresentationTools(); public static void main(String[] args) { logger.info("Starting JavaOne MCP Server"); // Set up STDIO transport var transportProvider = new STDIOServerTransportProvider(new ObjectMapper()); // Create MCP server MCPServer.serve(transportProvider) .serverInfo("JavaOne MCP Server", "0.0.1") .capabilities(MCPSchema.ServerCapabilities.builder() .tools(true) .logging(true) .build()) .tools(List.of(getSyncToolSpecification())) .build(); logger.info("Server running"); } private static MCPServerFeatures.SyncToolSpecification getSyncToolSpecification() { var schema = MCPSchema.object() .property("operation", MCPSchema.string()) .build(); return MCPServerFeatures.syncToolSpecification() .name("getPresentations") .description("Get a list of all presentations from JavaOne") .schema(schema) .implementation((exchange, arguments) -> { List<MCPSchema.Content> contents = new ArrayList<>(); for (Presentation presentation : presentationTools.getPresentations()) { contents.add(new MCPSchema.TextContent(presentation.toString())); } return new MCPSchema.CallToolResult(contents, false); }) .build(); } }
Here’s what’s happening:
  1. Logging: SLF4J is used to log server events, making debugging easier.
  2. Transport: The STDIOServerTransportProvider uses STDIO for communication, leveraging Jackson’s ObjectMapper (included via the MCP SDK).
  3. Tool Specification: The getSyncToolSpecification method defines a tool named getPresentations. It specifies a simple schema (an operation string) and an implementation that converts the Presentation list into MCP-compatible content.
  4. Server Configuration: The server is configured with a name, version, and capabilities (tools and logging enabled). The getPresentations tool is registered.

The implementation lambda transforms the Presentation objects into MCPSchema.TextContent for compatibility with MCP’s content model. If you wanted to add more tools (e.g., getPresentationsByYear), you’d create additional SyncToolSpecification instances.

Packaging the Application

The Maven Shade Plugin in the pom.xml ensures the JAR is executable. Build the project with:

mvn clean package

This generates a JAR file (e.g., JavaOneMCP-1.0-SNAPSHOT.jar) in the target directory.

Testing with MCP Inspector

The MCP Inspector tool, available in the MCP documentation, simplifies testing. Run it with:

npx mcp-inspector

In the MCP Inspector UI:

  1. Select STDIO as the transport type.
  2. Set the command to java -jar /path/to/JavaOneMCP-1.0-SNAPSHOT.jar.
  3. Click Connect.

The inspector should log the server starting (thanks to SLF4J). Use the List Tools feature to verify the getPresentations tool appears with its name and description. Run the tool to retrieve the list of presentations, which should match the hardcoded data in PresentationTools.

Integrating with an MCP Client

To demonstrate real-world usage, connect the server to an MCP client like Claude Desktop (or another compatible client). Configure the client to use the STDIO transport and point it to the JAR file. In the client, query:

Can you provide a list of all presentations from JavaOne?

The client will detect the getPresentations tool, execute it, and return the list of presentations, complete with titles, URLs, and years. This showcases how MCP enables tools to augment language models with custom functionality.

Extending the Example

To make the server more robust, consider these enhancements:

  • Additional Tools: Implement getPresentationsByYear as a new SyncToolSpecification to filter presentations by year.
  • Dynamic Data: Replace the hardcoded list with data from a database or external API.
  • Error Handling: Add validation and error responses in the tool implementation.
  • Alternative Transports: Use Server-Sent Events (SSE) instead of STDIO for web-based clients.

For example, here’s how you could add a getPresentationsByYear tool:

package dev.danvega;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.mcp.schema.MCPSchema;
import io.modelcontextprotocol.mcp.server.MCPServer;
import io.modelcontextprotocol.mcp.server.MCPServerFeatures;
import io.modelcontextprotocol.mcp.transport.STDIOServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

public class ExtendedApplication {

    private static final Logger logger = LoggerFactory.getLogger(ExtendedApplication.class);
    private static final PresentationTools presentationTools = new PresentationTools();

    public static void main(String[] args) {
        logger.info("Starting JavaOne MCP Server");

        var transportProvider = new STDIOServerTransportProvider(new ObjectMapper());

        MCPServer.serve(transportProvider)
                .serverInfo("JavaOne MCP Server", "0.0.1")
                .capabilities(MCPSchema.ServerCapabilities.builder()
                        .tools(true)
                        .logging(true)
                        .build())
                .tools(List.of(
                        getSyncToolSpecification(),
                        getPresentationsByYearSpecification()
                ))
                .build();

        logger.info("Server running");
    }

    private static MCPServerFeatures.SyncToolSpecification getSyncToolSpecification() {
        var schema = MCPSchema.object()
                .property("operation", MCPSchema.string())
                .build();

        return MCPServerFeatures.syncToolSpecification()
                .name("getPresentations")
                .description("Get a list of all presentations from JavaOne")
                .schema(schema)
                .implementation((exchange, arguments) -> {
                    List<MCPSchema.Content> contents = new ArrayList<>();
                    for (Presentation presentation : presentationTools.getPresentations()) {
                        contents.add(new MCPSchema.TextContent(presentation.toString()));
                    }
                    return new MCPSchema.CallToolResult(contents, false);
                })
                .build();
    }

    private static MCPServerFeatures.SyncToolSpecification getPresentationsByYearSpecification() {
        var schema = MCPSchema.object()
                .property("year", MCPSchema.integer())
                .build();

        return MCPServerFeatures.syncToolSpecification()
                .name("getPresentationsByYear")
                .description("Get a list of JavaOne presentations for a specific year")
                .schema(schema)
                .implementation((exchange, arguments) -> {
                    int year = arguments.get("year").getAsInt();
                    List<MCPSchema.Content> contents = new ArrayList<>();
                    for (Presentation presentation : presentationTools.getPresentationsByYear(year)) {
                        contents.add(new MCPSchema.TextContent(presentation.toString()));
                    }
                    return new MCPSchema.CallToolResult(contents, false);
                })
                .build();
    }
}

This adds a new tool that accepts a year parameter and filters the presentations accordingly. Update the pom.xml and rebuild to test the extended functionality.

Best Practices

  • Modularity: Keep tool specifications modular and reusable. Each tool should have a clear purpose and schema.
  • Logging: Use SLF4J extensively to log server events and errors, as it’s invaluable for debugging.
  • Testing: Always test with MCP Inspector before integrating with clients to catch issues early.
  • Documentation: Refer to the official MCP documentation at modelcontextprotocol.io for advanced features and updates.

Troubleshooting Common Issues

  • No Manifest Error: Ensure the Maven Shade Plugin is configured correctly in pom.xml with the main class specified.
  • Transport Issues: Verify the STDIO transport is compatible with your client. For web clients, consider switching to SSE.
  • Tool Not Found: Check the tool’s name and schema in the SyncToolSpecification. Ensure it’s added to the server’s tool list.

Why Use Pure Java?

Using the core Java SDK for MCP servers offers several advantages:

  • Lightweight: No external framework dependencies, reducing the application’s footprint.
  • Flexibility: Full control over the implementation, ideal for custom or minimalistic projects.
  • Learning: Deepens your understanding of MCP’s architecture without abstractions.

However, if your project already uses Spring, you can integrate optional Spring dependencies for features like dependency injection or web endpoints. The choice depends on your project’s needs.

Conclusion

Building an MCP server with pure Java is straightforward and powerful. By setting up a simple project, defining a data model, implementing a server, and testing with MCP Inspector, you can create a functional server that integrates seamlessly with MCP clients. The example here—serving JavaOne presentations—is a starting point. Extend it with more tools, dynamic data, or alternative transports to suit your use case. Check out modelcontextprotocol.io for more details on the Java SDK and advanced features. Happy coding!

0 comments:

Post a Comment