Model Context Protocol (MCP) and Integration with Quarkus

Model Context Protocol (MCP) and Integration with Quarkus

Introduction

The Model Context Protocol (MCP) has emerged as a critical standard for enabling AI models and agents to safely access and interact with external systems, data, and tools. For Java developers building enterprise applications with Quarkus, understanding how to implement MCP endpoints and integrate them with your microservices is essential. This article explores MCP concepts and demonstrates practical integration with Quarkus.

What is Model Context Protocol (MCP)?

The Model Context Protocol is a standardized specification for connecting AI models to external resources. It provides:

  • Standardized Integration: Consistent interfaces for AI-model-to-system communication
  • Resource Abstraction: Unified way to expose tools, resources, and prompts to AI systems
  • Security and Control: Well-defined permission boundaries and access control
  • Interoperability: Language-agnostic specification enabling diverse implementations

Core Concepts

Resources: Data or information accessible to the model

  • Read-only resources for data access
  • Versioning support for tracking changes
  • Template-based URI patterns

Tools: Functions the model can invoke

  • Semantic descriptions of tool capabilities
  • Input schemas and validation
  • Execution with proper error handling

Prompts: Pre-crafted instruction templates

  • Context-aware prompt generation
  • Dynamic parameter injection
  • Reusable prompt patterns

Why MCP Matters for Enterprise Applications

Safety and Control

MCP enables you to:

  • Define exactly what data and operations an AI model can access
  • Implement fine-grained permission controls
  • Maintain audit trails of AI model interactions
  • Prevent unauthorized access to sensitive resources

Integration Simplicity

Rather than custom integrations for each AI service:

  • Implement once, use with multiple AI platforms
  • Standardized request/response formats
  • Predictable error handling
  • Easier maintenance and evolution

Scalability

MCP supports:

  • Multiple concurrent client connections
  • Efficient resource sharing
  • Clear separation between business logic and integration
  • Easy horizontal scaling

Implementing MCP with Quarkus

Project Setup

Create a Quarkus project with necessary extensions:

quarkus create app mcp-quarkus \
  -x resteasy-reactive-jackson

MCP Server Implementation

Here’s a practical example of an MCP server exposing database resources:

import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.json.Json;
import jakarta.json.JsonObject;

@ApplicationScoped
public class MCPServerConfig {
    
    @GET
    @Path("/mcp/initialize")
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject initialize() {
        return Json.createObjectBuilder()
            .add("protocolVersion", "2024-11-05")
            .add("capabilities", Json.createObjectBuilder()
                .add("resources", true)
                .add("tools", true)
                .add("prompts", true)
                .build())
            .add("serverInfo", Json.createObjectBuilder()
                .add("name", "Quarkus MCP Server")
                .add("version", "1.0.0")
                .build())
            .build();
    }
}

Exposing Resources via MCP

Expose your application’s data as MCP resources:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.persistence.EntityManager;
import jakarta.json.JsonObject;

@Path("/mcp/resources")
@ApplicationScoped
public class MCPResourcesEndpoint {
    
    @Inject
    EntityManager em;
    
    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject listResources() {
        return Json.createObjectBuilder()
            .add("resources", Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                    .add("uri", "quarkus://database/users")
                    .add("name", "Users Database")
                    .add("description", "Access to user profiles and information")
                    .add("mimeType", "application/json")
                    .build())
                .build())
            .build();
    }
    
    @POST
    @Path("/read")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject readResource(JsonObject request) {
        String resourceUri = request.getString("uri");
        
        if (resourceUri.equals("quarkus://database/users")) {
            // Query user data
            var users = em.createQuery(
                "SELECT u FROM User u", 
                User.class)
                .getResultList();
            
            return Json.createObjectBuilder()
                .add("resourceUri", resourceUri)
                .add("contents", Json.createArrayBuilder()
                    .add(Json.createObjectBuilder()
                        .add("mimeType", "application/json")
                        .add("text", users.toString())
                        .build())
                    .build())
                .build();
        }
        
        throw new BadRequestException("Unknown resource");
    }
}

Implementing MCP Tools

Expose application functions as MCP tools:

@Path("/mcp/tools")
@ApplicationScoped
public class MCPToolsEndpoint {
    
    @Inject
    UserService userService;
    
    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject listTools() {
        return Json.createObjectBuilder()
            .add("tools", Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                    .add("name", "get_user_by_email")
                    .add("description", "Retrieve user information by email address")
                    .add("inputSchema", Json.createObjectBuilder()
                        .add("type", "object")
                        .add("properties", Json.createObjectBuilder()
                            .add("email", Json.createObjectBuilder()
                                .add("type", "string")
                                .add("description", "User's email address")
                                .build())
                            .build())
                        .add("required", Json.createArrayBuilder()
                            .add("email")
                            .build())
                        .build())
                    .build())
                .build())
            .build();
    }
    
    @POST
    @Path("/call")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject callTool(JsonObject request) {
        String toolName = request.getString("name");
        JsonObject arguments = request.getJsonObject("arguments");
        
        return switch (toolName) {
            case "get_user_by_email" -> {
                String email = arguments.getString("email");
                var user = userService.findByEmail(email);
                yield Json.createObjectBuilder()
                    .add("content", Json.createArrayBuilder()
                        .add(Json.createObjectBuilder()
                            .add("type", "text")
                            .add("text", user.toString())
                            .build())
                        .build())
                    .add("isError", false)
                    .build();
            }
            default -> Json.createObjectBuilder()
                .add("isError", true)
                .add("content", Json.createArrayBuilder()
                    .add(Json.createObjectBuilder()
                        .add("type", "text")
                        .add("text", "Unknown tool: " + toolName)
                        .build())
                    .build())
                .build();
        };
    }
}

MCP Prompts Implementation

Create reusable prompts for AI models:

@Path("/mcp/prompts")
@ApplicationScoped
public class MCPPromptsEndpoint {
    
    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject listPrompts() {
        return Json.createObjectBuilder()
            .add("prompts", Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                    .add("name", "user_analysis")
                    .add("description", "Analyze user activity and engagement patterns")
                    .add("arguments", Json.createArrayBuilder()
                        .add(Json.createObjectBuilder()
                            .add("name", "timeframe")
                            .add("description", "Analysis period (e.g., 'last_30_days')")
                            .build())
                        .build())
                    .build())
                .build())
            .build();
    }
    
    @POST
    @Path("/get")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject getPrompt(JsonObject request) {
        String promptName = request.getString("name");
        JsonObject arguments = request.getJsonObject("arguments");
        
        if ("user_analysis".equals(promptName)) {
            String timeframe = arguments.getString("timeframe", "last_30_days");
            
            return Json.createObjectBuilder()
                .add("messages", Json.createArrayBuilder()
                    .add(Json.createObjectBuilder()
                        .add("role", "user")
                        .add("content", "Analyze user engagement for " + timeframe + 
                            ". Focus on activity patterns, retention, and feature usage.")
                        .build())
                    .build())
                .build();
        }
        
        throw new BadRequestException("Unknown prompt: " + promptName);
    }
}

Security Best Practices

Authentication

@ApplicationScoped
public class MCPAuthFilter implements ContainerRequestFilter {
    
    @Override
    public void filter(ContainerRequestContext requestContext) 
            throws IOException {
        String token = requestContext.getHeaderString("Authorization");
        
        if (!validateToken(token)) {
            requestContext.abortWith(Response.status(401).build());
        }
    }
    
    private boolean validateToken(String token) {
        // Implement your authentication logic
        return token != null && !token.isEmpty();
    }
}

Input Validation

@ApplicationScoped
public class InputValidator {
    
    public void validateToolCall(String toolName, JsonObject args) {
        if (!isAllowedTool(toolName)) {
            throw new SecurityException("Tool not allowed: " + toolName);
        }
        
        // Validate argument types and values
        // Prevent injection attacks
        // Check rate limits
    }
}

Testing MCP Endpoints

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;

@QuarkusTest
public class MCPEndpointTest {
    
    @Test
    public void testInitialize() {
        given()
            .when()
            .get("/mcp/initialize")
            .then()
            .statusCode(200)
            .body("serverInfo.name", equalTo("Quarkus MCP Server"));
    }
    
    @Test
    public void testToolCall() {
        given()
            .contentType(ContentType.JSON)
            .body("{\"name\":\"get_user_by_email\",\"arguments\":{\"email\":\"test@example.com\"}}")
            .when()
            .post("/mcp/tools/call")
            .then()
            .statusCode(200)
            .body("isError", equalTo(false));
    }
}

Best Practices for MCP with Quarkus

1. Resource Versioning

Include version information in resource URIs to handle schema evolution:

// Good: versioned resource
"uri", "quarkus://database/users/v2"

// Allows maintaining backward compatibility

2. Pagination for Large Resources

@POST
@Path("/read")
public JsonObject readResource(JsonObject request) {
    int offset = request.getInt("offset", 0);
    int limit = request.getInt("limit", 100);
    // Return paginated results
}

3. Rate Limiting

Implement rate limiting on tool calls:

@ApplicationScoped
public class RateLimiter {
    private final Map<String, Integer> callCounts = new ConcurrentHashMap<>();
    
    public boolean isAllowed(String clientId) {
        int count = callCounts.getOrDefault(clientId, 0);
        if (count > 100) { // 100 calls per minute
            return false;
        }
        callCounts.put(clientId, count + 1);
        return true;
    }
}

4. Comprehensive Logging

@ApplicationScoped
public class MCPAuditLogger {
    
    public void logToolCall(String toolName, JsonObject args, 
                           JsonObject result, String clientId) {
        LOGGER.info("MCP Tool Call - Tool: {}, Client: {}, Args: {}, Result: {}",
            toolName, clientId, args, result);
    }
}

Conclusion

The Model Context Protocol provides a robust, standardized way to integrate AI capabilities into enterprise Java applications. Quarkus, with its lightweight nature and excellent performance characteristics, is an ideal platform for implementing MCP servers. By following the patterns and practices outlined in this article, you can build secure, scalable MCP integrations that enable AI models to safely access your business data and operations.

As AI integration becomes increasingly common in enterprise systems, MCP will play a vital role in ensuring these integrations remain secure, auditable, and maintainable.

Share: X (Twitter) Facebook LinkedIn