9 minuto(s) de lectura

Con tantos años de Java, cuando empecé a explorar MCP (Model Context Protocol) me encontré con que el 90% de los ejemplos y servidores están escritos en TypeScript o Python. Y está bien, son buenos lenguajes para este tipo de cosas. Pero si sos del ecosistema Java, ¿por qué mudarse?

Resulta que Spring AI ya tiene soporte MCP integrado. Así que decidí construir algo útil para probarlo: un servidor MCP que consulta datos financieros en tiempo real (acciones y criptomonedas) usando Java 21 y Spring Boot.

Qué es MCP (versión corta)

MCP es un protocolo que permite a asistentes de IA (como Claude) conectarse con herramientas externas de forma estandarizada. Es como darle a Claude acceso directo a tus datos y servicios sin tener que copiar y pegar todo el tiempo.

Si querés entender en detalle qué es MCP y por qué es útil, te recomiendo leer mi post anterior: Demoliendo Paredes

La versión ultra resumida: Claude puede invocar herramientas (tools) de tu servidor MCP para hacer cosas que normalmente no podría hacer (consultar APIs, leer bases de datos, etc.).

Por qué Java para MCP

Antes de meternos en el código, vale la pena preguntarse: ¿tiene sentido usar Java para esto?

Ventajas

1. Type Safety desde el minuto cero

// Errores en tiempo de compilación, no en producción
public StockQuote getStockQuote(String symbol) {
    // El compilador valida todo
}

2. Ecosistema empresarial maduro

  • Spring Boot para configuración e inyección de dependencias
  • WebClient reactivo para llamadas HTTP
  • Validación integrada

3. Performance y escalabilidad

  • Virtual Threads (Java 21) para concurrencia sin overhead
  • JVM optimizada para cargas pesadas
  • Deployment sencillo (un JAR y listo)

4. Spring AI MCP integrado No necesitás bibliotecas de terceros. Spring AI ya trae soporte MCP oficial.

Cuándo NO usar Java

Seamos honestos. Java no es siempre la mejor opción:

  • Si necesitás prototipar rápido, TypeScript/Python seguramente te resulte más agil
  • Si tu stack ya es completamente JavaScript, quedarte ahí tiene sentido
  • Si el proyecto es personal y simple, Java puede ser overkill

Pero si ya estás en el ecosistema Java/Spring, construir servidores MCP es totalmente viable.

El Proyecto: MCP Financial Server

Un servidor MCP que expone tres herramientas:

  1. get_stock_price: Consulta precio de acciones (vía Alpha Vantage)
  2. get_crypto_price: Consulta precio de criptomonedas (vía CoinGecko)
  3. get_market_summary: Resumen rápido del mercado

La idea es que Claude pueda responder preguntas como:

  • “¿Cuál es el precio actual de Apple?”
  • “¿Cuánto vale Bitcoin ahora?”
  • “Mostrame un resumen del mercado”

Arquitectura del Proyecto

mcp-financial-server/
├── tools/              # Herramientas MCP expuestas
│   ├── StockPriceTool.java
│   ├── CryptoPriceTool.java
│   └── MarketSummaryTool.java
├── services/           # Lógica de negocio
│   ├── StockService.java
│   └── CryptoService.java
├── models/             # DTOs
│   ├── StockQuote.java
│   └── CryptoQuote.java
└── config/             # Configuración Spring
    ├── ApiConfiguration.java
    └── WebClientConfig.java

Flujo de Ejecución

Claude Desktop → Servidor MCP → Tool → Service → API Externa
                                                  (Alpha Vantage / CoinGecko)

Implementación Paso a Paso

Paso 1: Dependencies

<!-- pom.xml -->
<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.1.0-M2</spring-ai.version>
</properties>

<dependencies>
    <!-- Spring AI MCP Server -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server</artifactId>
    </dependency>
    
    <!-- WebFlux para llamadas HTTP reactivas -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    <!-- Lombok para reducir boilerplate -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

Paso 2: Configuración MCP

# application.yml
spring:
  application:
    name: mcp-financial-server
    
  # IMPORTANTE: Configuración para modo MCP (stdio)
  main:
    banner-mode: off
    web-application-type: none
    
  ai:
    mcp:
      server:
        name: financial-data-server
        version: 1.0.0
        transport: stdio  # Comunicación vía stdin/stdout

# Configuración de APIs externas
financial:
  apis:
    alpha-vantage:
      base-url: https://www.alphavantage.co/query
      api-key: ${ALPHA_VANTAGE_API_KEY:demo}
      timeout-seconds: 10
    
    coingecko:
      base-url: https://api.coingecko.com/api/v3
      timeout-seconds: 10

# CRÍTICO: Deshabilitar logging porque MCP usa stdio
logging:
  level:
    root: OFF

Detalle importante: MCP se comunica vía stdio (entrada/salida estándar). Si hacés System.out.println() o loggeás a consola, rompés el protocolo. Por eso web-application-type: none y logging: OFF.

Paso 3: Modelo de Datos

// StockQuote.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StockQuote {
    private String symbol;
    private String name;
    private BigDecimal price;
    private BigDecimal change;
    
    @JsonProperty("change_percent")
    private BigDecimal changePercent;
    
    @JsonProperty("open_price")
    private BigDecimal openPrice;
    
    @JsonProperty("high_price")
    private BigDecimal highPrice;
    
    @JsonProperty("low_price")
    private BigDecimal lowPrice;
    
    private Long volume;
    
    @JsonProperty("last_updated")
    private String lastUpdated;
    
    public static StockQuote error(String symbol, String errorMessage) {
        return StockQuote.builder()
                .symbol(symbol)
                .name("Error: " + errorMessage)
                .build();
    }
}

Lombok se encarga de getters, setters, builder y constructores. @JsonProperty mapea los nombres al formato snake_case que esperan las APIs.

Paso 4: Service Layer

// StockService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class StockService {
    
    private final WebClient.Builder webClientBuilder;
    private final ApiConfiguration apiConfig;
    
    public StockQuote getStockQuote(String symbol) {
        log.debug("Consultando cotización para símbolo: {}", symbol);
        
        try {
            WebClient webClient = webClientBuilder
                    .baseUrl(apiConfig.getAlphaVantage().getBaseUrl())
                    .build();
            
            JsonNode response = webClient.get()
                    .uri(uriBuilder -> uriBuilder
                            .queryParam("function", "GLOBAL_QUOTE")
                            .queryParam("symbol", symbol)
                            .queryParam("apikey", apiConfig.getAlphaVantage().getApiKey())
                            .build())
                    .retrieve()
                    .bodyToMono(JsonNode.class)
                    .timeout(Duration.ofSeconds(apiConfig.getAlphaVantage().getTimeoutSeconds()))
                    .block();
            
            return parseStockQuote(symbol, response);
            
        } catch (Exception e) {
            log.error("Error al consultar cotización para {}: {}", symbol, e.getMessage());
            return StockQuote.error(symbol, e.getMessage());
        }
    }
    
    private StockQuote parseStockQuote(String symbol, JsonNode response) {
        // Validar errores de la API
        if (response.has("Error Message")) {
            return StockQuote.error(symbol, response.get("Error Message").asText());
        }
        
        if (response.has("Note")) {
            return StockQuote.error(symbol, response.get("Note").asText());
        }
        
        JsonNode quote = response.get("Global Quote");
        if (quote == null || quote.isEmpty()) {
            return StockQuote.error(symbol, "No se encontraron datos");
        }
        
        // Construir el objeto StockQuote
        return StockQuote.builder()
                .symbol(quote.path("01. symbol").asText())
                .price(new BigDecimal(quote.path("05. price").asText()))
                .change(new BigDecimal(quote.path("09. change").asText()))
                .changePercent(parseChangePercent(quote.path("10. change percent").asText()))
                .openPrice(new BigDecimal(quote.path("02. open").asText()))
                .highPrice(new BigDecimal(quote.path("03. high").asText()))
                .lowPrice(new BigDecimal(quote.path("04. low").asText()))
                .volume(quote.path("06. volume").asLong())
                .lastUpdated(quote.path("07. latest trading day").asText())
                .name(symbol)
                .build();
    }
    
    private BigDecimal parseChangePercent(String changePercent) {
        if (changePercent == null || changePercent.isEmpty()) {
            return BigDecimal.ZERO;
        }
        return new BigDecimal(changePercent.replace("%", ""));
    }
}

Patrón aplicado: Service Layer Pattern. La lógica de negocio está separada del tool MCP. El servicio no sabe nada de MCP, solo hace su trabajo.

Paso 5: MCP Tool

// StockPriceTool.java
@Slf4j
@Service
@RequiredArgsConstructor
public class StockPriceTool {
    
    private final StockService stockService;
    private final ObjectMapper objectMapper;
    
    @Tool(description = "Obtiene el precio actual y datos de mercado de una acción. " +
            "Proporciona información como precio actual, cambio porcentual, " +
            "volumen y precios máximo/mínimo del día. " +
            "Ejemplos de símbolos: AAPL (Apple), GOOGL (Google), MSFT (Microsoft), TSLA (Tesla).")
    public String getStockPrice(String symbol) {
        log.info("Ejecutando tool get_stock_price para símbolo: {}", symbol);
        
        try {
            // Validar input
            if (symbol == null || symbol.isBlank()) {
                return formatError("El símbolo de la acción es requerido");
            }
            
            // Normalizar símbolo a mayúsculas
            String normalizedSymbol = symbol.trim().toUpperCase();
            
            // Llamar al servicio
            StockQuote quote = stockService.getStockQuote(normalizedSymbol);
            
            // Serializar a JSON
            return objectMapper.writerWithDefaultPrettyPrinter()
                    .writeValueAsString(quote);
                    
        } catch (Exception e) {
            log.error("Error en get_stock_price: {}", e.getMessage(), e);
            return formatError("Error al consultar precio de acción: " + e.getMessage());
        }
    }
    
    private String formatError(String message) {
        try {
            return objectMapper.writerWithDefaultPrettyPrinter()
                    .writeValueAsString(StockQuote.error("N/A", message));
        } catch (Exception e) {
            return "{\"error\": \"" + message + "\"}";
        }
    }
}

La magia: @Tool es una anotación de Spring AI que registra este método como herramienta MCP. El description es lo que Claude ve para entender qué hace la herramienta.

Detalles importantes:

  • El método debe retornar String (JSON serializado)
  • Los parámetros se mapean automáticamente desde la llamada MCP
  • Spring se encarga de la inyección de dependencias (StockService)

Integración con Claude Desktop

Una vez compilado, hay que decirle a Claude Desktop que existe este servidor.

1. Compilar el JAR

mvn clean package

Esto genera: target/spring-mcp-financial-demo-1.0.0.jar

2. Configurar Claude Desktop

Editar el archivo de configuración:

En Mac: ~/Library/Application Support/Claude/claude_desktop_config.json

En Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "financial-data": {
      "command": "java",
      "args": [
        "-jar",
        "/ruta/completa/al/spring-mcp-financial-demo-1.0.0.jar"
      ],
      "env": {
        "ALPHA_VANTAGE_API_KEY": "tu_api_key_aqui"
      }
    }
  }
}

3. Reiniciar Claude Desktop

Cerrar completamente y volver a abrir. Si todo está bien, deberías ver un icono de herramientas indicando que el servidor MCP está conectado.

4. Probar

En Claude Desktop:

  • “¿Cuál es el precio actual de Apple?”
  • “Mostrame el resumen del mercado”
  • “¿Cuánto vale Bitcoin ahora?”

Claude invocará automáticamente las herramientas del servidor MCP.

Errores Comunes

1. API Key inválida

Síntoma: Alpha Vantage retorna errores o datos limitados

Solución: Obtener una API key gratuita en https://www.alphavantage.co/support/#api-key

La API key gratuita tiene límites:

  • 25 requests por día
  • 5 requests por minuto

Si necesitás más, considerá cachear respuestas o usar una cuenta premium.

2. Logs contaminando stdio

Síntoma: El servidor no responde o Claude muestra errores

Problema: Si hacés System.out.println() o loggeás a consola, MCP se rompe porque espera JSON puro en stdout.

Solución: Asegurate de que en application.yml tengas:

logging:
  level:
    root: OFF

Y en el McpFinancialServerApplication.java:

public static void main(String[] args) {
    // IMPORTANTE: No usar log.info() aquí
    // Los logs van a archivo, no a consola
    SpringApplication.run(McpFinancialServerApplication.class, args);
}

3. Path incorrecto en claude_desktop_config.json

Síntoma: Claude Desktop no conecta con el servidor

Solución: Usar la ruta ABSOLUTA al JAR:

"/Users/tu_usuario/dev/proyecto/target/spring-mcp-financial-demo-1.0.0.jar"

No usar rutas relativas ni ~.

4. Rate limiting de APIs

Síntoma: Después de varias consultas, empiezan a fallar

Problema: Alpha Vantage tiene límites estrictos en el plan gratuito

Soluciones:

  • Implementar cache de respuestas
  • Usar una cuenta premium
  • Cambiar a otra API (Yahoo Finance, IEX Cloud, etc.)

Patrones de Diseño Aplicados

1. Dependency Injection

Spring inyecta automáticamente las dependencias:

@RequiredArgsConstructor  // Lombok genera el constructor
public class StockPriceTool {
    private final StockService stockService;
    private final ObjectMapper objectMapper;
    // Spring inyecta estas dependencias automáticamente
}

Sin Spring, tendrías que hacer:

StockService stockService = new StockService(new WebClient.Builder(), config);
ObjectMapper objectMapper = new ObjectMapper();
StockPriceTool tool = new StockPriceTool(stockService, objectMapper);

Con Spring, es transparente.

2. Builder Pattern

Lombok genera builders para los DTOs:

StockQuote quote = StockQuote.builder()
    .symbol("AAPL")
    .price(new BigDecimal("178.25"))
    .change(new BigDecimal("2.15"))
    .build();

Más legible que constructores largos.

3. Service Layer Pattern

La lógica de negocio está separada:

Tool (capa de presentación MCP)
  → Service (lógica de negocio)
    → API Externa

Esto permite:

  • Testear el servicio sin MCP
  • Reutilizar el servicio en otros contextos (REST API, CLI, etc.)
  • Cambiar la API externa sin tocar el tool

4. Configuration Properties

Las configuraciones están externalizadas:

@Data
@Configuration
@ConfigurationProperties(prefix = "financial.apis")
public class ApiConfiguration {
    private AlphaVantageConfig alphaVantage;
    private CoinGeckoConfig coingecko;
    
    @Data
    public static class AlphaVantageConfig {
        private String baseUrl;
        private String apiKey;
        private int timeoutSeconds;
    }
}

Spring mapea automáticamente desde application.yml. Cambiar de entorno (dev, prod) es solo cambiar el archivo de configuración.

Siguientes Pasos

Ideas para extender el proyecto

1. Datos históricos

@Tool(description = "Obtiene datos históricos de una acción")
public String getHistoricalData(String symbol, int days) {
    // Llamar a TIME_SERIES_DAILY de Alpha Vantage
}

2. Indicadores técnicos

@Tool(description = "Calcula RSI de una acción")
public String calculateRSI(String symbol, int period) {
    // Integrar con Alpha Vantage Technical Indicators
}

3. Cache de respuestas

@Service
@CacheConfig(cacheNames = "stock-quotes")
public class StockService {
    
    @Cacheable(key = "#symbol")
    public StockQuote getStockQuote(String symbol) {
        // Cache por 1 minuto para reducir llamadas a API
    }
}

4. Múltiples fuentes de datos

  • Yahoo Finance
  • IEX Cloud
  • Finnhub
  • Polygon.io

Crear una abstracción que permita cambiar entre proveedores.

5. Testing

@SpringBootTest
class StockServiceTest {
    
    @Autowired
    private StockService stockService;
    
    @Test
    void shouldGetStockQuote() {
        StockQuote quote = stockService.getStockQuote("AAPL");
        assertThat(quote.getSymbol()).isEqualTo("AAPL");
        assertThat(quote.getPrice()).isNotNull();
    }
}

Conclusiones

MCP en Java no solo es viable, sino que tiene ventajas significativas:

  • Type safety que previene bugs en producción
  • Ecosistema maduro con Spring Boot
  • Spring AI integrado oficialmente
  • Mantenibilidad a largo plazo

Si ya estás en el ecosistema Java/Spring, no necesitás mudarte a TypeScript o Python para construir servidores MCP. Spring AI te da todo lo que necesitás.

El proyecto demuestra que podés construir herramientas MCP robustas aprovechando las ventajas del stack Java: inyección de dependencias, configuración externalizada, validación automática, y todo el ecosistema de Spring.

Recursos

Código Fuente

Referencias

Posts Relacionados

Deja un comentario