LangChain4j 연동을 위한 자바 MCP 서비스 개발

MCP(Model Context Protocol)는 대규모 언어 모델(LLM)이 외부 도구와 상호작용하기 위한 경량 프로토콜입니다. 자바 환경에서 MCP 기반의 도구를 구현하려면, 이 프로토콜을 준수하는 백엔드 서비스를 구축하고 LangChain4j와 같은 클라이언트가 표준 입출력(Stdio) 또는 HTTP/SSE를 통해 이 서비스를 호출할 수 있도록 해야 합니다. 다음은 상세 구현 과정과 코드 예시입니다.

MCP 도구의 핵심 개념

MCP 도구는 다음과 같은 기능을 구현해야 합니다:

  • 도구 명세: 이름, 설명, 매개변수 등 도구의 기능을 선언하는 메타데이터를 제공합니다.
  • 실행 로직: JSON 형식의 입력을 받아 처리하고, JSON 형식의 결과를 반환합니다.
  • 통신 방식: 로컬 프로세스를 위한 Stdio 방식과 원격 서비스를 위한 HTTP/SSE 방식을 지원합니다.

개발 단계

1단계: 도구 메타데이터 정의

각 도구는 name, description, parameters를 포함하는 명세를 제공해야 합니다. 다음은 간단한 텍스트 번역 도구의 예시입니다:

{
  "name": "korean_to_english_translator",
  "description": "한국어 텍스트를 영어로 번역하는 도구",
  "parameters": {
    "type": "object",
    "properties": {
      "input_string": {
        "type": "string",
        "description": "번역할 한국어 문장"
      }
    },
    "required": ["input_string"]
  }
}

2단계: 도구 서버 구현

옵션 1: Stdio 기반 로컬 도구 (빠른 개발 권장)

Stdio 모드는 도구가 독립적인 프로세스로 실행되며, 표준 입력 및 출력을 통해 LangChain4j와 통신합니다. 이 방식은 개발 및 테스트에 용이합니다.

예제 코드 (자바 + Jackson 라이브러리):

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; // List.of는 Java 9+
import java.util.Map;
import java.util.Scanner;

public class LocalTranslationService {

    private static final ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        Scanner stdInputScanner = new Scanner(System.in);
        while (stdInputScanner.hasNextLine()) {
            String receivedLine = stdInputScanner.nextLine();
            try {
                // MCP 클라이언트 요청 분석
                Map<String, Object> clientRequest = jsonMapper.readValue(receivedLine, Map.class);
                String actionCommand = (String) clientRequest.get("command");

                if ("discover".equals(actionCommand)) {
                    // 도구 메타데이터 응답
                    Map<String, Object> toolDescription = new HashMap<>();
                    toolDescription.put("name", "korean_to_english_translator");
                    toolDescription.put("description", "한국어 텍스트를 영어로 번역하는 도구");
                    toolDescription.put("parameters", Map.of(
                        "type", "object",
                        "properties", Map.of(
                            "input_string", Map.of("type", "string", "description", "번역할 한국어 문장")
                        ),
                        "required", Arrays.asList("input_string")
                    ));
                    System.out.println(jsonMapper.writeValueAsString(toolDescription));
                } else if ("execute".equals(actionCommand)) {
                    // 도구의 핵심 로직 실행
                    Map<String, Object> executionArgs = (Map<String, Object>) clientRequest.get("args");
                    String inputSentence = (String) executionArgs.get("input_string");
                    String translatedSentence = simulateTranslation(inputSentence);

                    // 결과 응답
                    Map<String, Object> executionResult = new HashMap<>();
                    executionResult.put("output", translatedSentence); // 결과 키 변경
                    System.out.println(jsonMapper.writeValueAsString(executionResult));
                }
            } catch (Exception e) {
                // 오류 발생 시 표준 에러 출력
                try {
                    Map<String, Object> errorPayload = Map.of("error", "도구 처리 오류: " + e.getMessage());
                    System.err.println(jsonMapper.writeValueAsString(errorPayload));
                } catch (Exception jsonError) {
                    System.err.println("JSON 직렬화 오류: " + jsonError.getMessage());
                }
            }
        }
    }

    private static String simulateTranslation(String text) {
        // 실제 번역 서비스(예: Papago, Google Translate API)와 연동 가능
        if (text.contains("안녕하세요")) {
            return "Hello";
        } else if (text.contains("세상")) {
            return "World";
        } else if (text.contains("고맙습니다")) {
            return "Thank you";
        }
        return "Translation for this phrase is not yet supported.";
    }
}
옵션 2: HTTP/SSE 기반 원격 도구

원격에서 도구를 호출해야 하는 경우, Spring Boot를 활용하여 HTTP 인터페이스를 구현할 수 있습니다. 이는 분산 시스템 환경에 적합합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Arrays; // Arrays.asList를 위해 추가

@SpringBootApplication
@RestController
@RequestMapping("/api/mcp-tool") // 베이스 경로 변경
public class RemoteTranslationTool {

    public static void main(String[] args) {
        SpringApplication.run(RemoteTranslationTool.class, args);
    }

    @GetMapping("/metadata") // 도구 명세 엔드포인트 변경
    public Map<String, Object> getToolMetadata() {
        return Map.of(
            "name", "korean_to_english_translator",
            "description", "HTTP를 통해 한국어를 영어로 번역하는 원격 도구",
            "parameters", Map.of(
                "type", "object",
                "properties", Map.of(
                    "input_string", Map.of("type", "string", "description", "번역할 한국어 문장")
                ),
                "required", Arrays.asList("input_string")
            )
        );
    }

    @PostMapping("/execute-translation") // 실행 엔드포인트 변경
    public Map<String, Object> executeToolLogic(@RequestBody Map<String, Object> arguments) {
        String inputSentence = (String) arguments.get("input_string");
        String translatedSentence = simulateTranslation(inputSentence);
        return Map.of("translated_text", translatedSentence); // 결과 키 변경
    }

    private String simulateTranslation(String text) {
        // 실제 번역 로직
        if (text.contains("안녕하세요")) return "Hello";
        if (text.contains("세상")) return "World";
        if (text.contains("고맙습니다")) return "Thank you";
        return "Translation for this phrase is not yet supported.";
    }
}

3단계: LangChain4j에서 MCP 도구 호출

LangChain4j는 MCP 도구를 손쉽게 통합할 수 있는 기능을 제공합니다.

방법 1: Stdio를 통해 로컬 도구 호출
import dev.langchain4j.agent.tool.McpToolProvider;
import dev.langchain4j.agent.tool.ToolProvider;
import dev.langchain4j.agent.tool.McpClient;
import dev.langchain4j.agent.tool.DefaultMcpClient;
import dev.langchain4j.agent.tool.StdioMcpTransport;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import java.util.Arrays;
import java.util.List;

public class LangChain4jLocalToolIntegration {
    public static void main(String[] args) throws Exception {
        // 1. MCP 도구 프로세스 시작 (예: JAR 파일 실행)
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", "local-translation-service.jar");
        Process toolProcess = processBuilder.start();

        // 2. Stdio 통신 계층 구성
        StdioMcpTransport stdioComm = StdioMcpTransport.builder()
                .process(toolProcess)
                .build();

        // 3. MCP 클라이언트 인스턴스 생성
        McpClient mcpLocalClient = new DefaultMcpClient.Builder()
                .transport(stdioComm)
                .build();

        // 4. LangChain4j의 ToolProvider에 MCP 클라이언트 등록
        ToolProvider localToolProvider = McpToolProvider.builder()
                .mcpClients(Arrays.asList(mcpLocalClient))
                .build();

        // 5. AI 서비스 및 LLM 초기화
        ChatLanguageModel llmInstance = OpenAiChatModel.builder()
                .apiKey("YOUR_OPENAI_API_KEY") // 실제 OpenAI API 키로 교체
                .build();

        // 번역 작업을 수행할 AI 서비스 인터페이스 정의
        interface TranslatorAgent {
            String performTranslation(String text);
        }

        TranslatorAgent aiServiceAgent = AiServices.builder(TranslatorAgent.class)
                .chatLanguageModel(llmInstance)
                .toolProvider(localToolProvider)
                .build();

        // 6. AI 에이전트 실행 및 결과 확인
        String translationResult = aiServiceAgent.performTranslation("안녕하세요 세상입니다");
        System.out.println("AI 에이전트 응답 (Stdio): " + translationResult); // 예상 출력: Hello World

        // 프로세스 종료 (자원 해제)
        toolProcess.destroy();
    }
}
방법 2: HTTP를 통해 원격 도구 호출
import dev.langchain4j.agent.tool.HttpMcpTransport;
import dev.langchain4j.agent.tool.McpClient;
import dev.langchain4j.agent.tool.McpToolProvider;
import dev.langchain4j.agent.tool.DefaultMcpClient;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import java.util.Arrays;
import java.util.List;

public class LangChain4jRemoteToolIntegration {
    public static void main(String[] args) {
        // 1. HTTP 통신 계층 구성
        HttpMcpTransport httpComm = HttpMcpTransport.builder()
                .url("http://localhost:8080/api/mcp-tool") // 변경된 원격 도구 URL 사용
                .build();

        // 2. MCP 클라이언트 인스턴스 생성
        McpClient mcpRemoteClient = new DefaultMcpClient.Builder()
                .transport(httpComm)
                .build();

        // 3. LangChain4j의 ToolProvider에 MCP 클라이언트 등록
        ToolProvider remoteToolProvider = McpToolProvider.builder()
                .mcpClients(Arrays.asList(mcpRemoteClient))
                .build();

        // 4. AI 서비스 및 LLM 초기화 (Stdio 방식과 동일)
        ChatLanguageModel llmInstance = OpenAiChatModel.builder()
                .apiKey("YOUR_OPENAI_API_KEY") // 실제 OpenAI API 키로 교체
                .build();

        interface RemoteTranslatorAgent {
            String processRemoteTranslation(String query); // 메소드명 변경
        }

        RemoteTranslatorAgent remoteAiServiceAgent = AiServices.builder(RemoteTranslatorAgent.class)
                .chatLanguageModel(llmInstance)
                .toolProvider(remoteToolProvider)
                .build();

        // 5. AI 에이전트 실행 및 결과 확인
        String remoteResult = remoteAiServiceAgent.processRemoteTranslation("고맙습니다");
        System.out.println("AI 에이전트 응답 (HTTP): " + remoteResult); // 예상 출력: Thank you
    }
}

주요 구현 요약

  1. 도구 명세: name, description, parameters를 포함하여 LLM이 도구의 목적을 이해하도록 돕습니다.
  2. 실행 로직: JSON 입력을 받아 JSON 결과로 반환하며, MCP 프로토콜의 형식에 맞춰야 합니다.
  3. 통신 방식:
    • Stdio: 로컬 프로세스 간 표준 입출력 통신에 적합합니다.
    • HTTP/SSE: 원격 서비스 호출에 사용되며, REST API 또는 SSE(Server-Sent Events)를 통해 이루어집니다.
  4. 오류 처리: 오류 발생 시 { "error": "메시지" }와 같은 표준화된 오류 응답 형식을 따르는 것이 좋습니다.

도구 테스트

Stdio 도구 수동 테스트
# Stdio 도구 컴파일 후 실행
# $ javac LocalTranslationService.java
# $ jar -cvf local-translation-service.jar LocalTranslationService.class
# $ java -jar local-translation-service.jar

# 위 명령어를 실행한 터미널에서 다음 JSON 라인을 직접 입력하거나 파이프를 통해 전달합니다:

# 도구 명세 요청:
{"command": "discover"}
# (Enter 키 입력)

# 도구 실행 요청:
{"command": "execute", "args": {"input_string": "안녕하세요 세상입니다"}}
# (Enter 키 입력)

참고: `java -jar` 명령어를 실행한 후, 같은 터미널에 JSON 문자열을 직접 붙여넣거나 `echo '{"command": "discover"}' | java -jar local-translation-service.jar` 와 같이 파이프를 사용하여 입력할 수 있습니다.

HTTP 도구 수동 테스트
# Spring Boot HTTP 도구 실행 (애플리케이션을 시작한 후)

# 도구 명세 확인
curl http://localhost:8080/api/mcp-tool/metadata

# 번역 실행 요청
curl -X POST -H "Content-Type: application/json" -d '{"input_string": "고맙습니다"}' http://localhost:8080/api/mcp-tool/execute-translation

추가 최적화 방안

  • 스트리밍 출력 지원: SSE(Server-Sent Events)를 활용하여 실시간으로 결과를 스트리밍합니다.
  • 실제 번역 API 통합: 네이버 파파고, 구글 번역 등 실제 번역 서비스 API를 연동하여 번역 품질을 향상시킵니다.
  • 로깅 및 모니터링: 도구 호출 및 실행에 대한 상세 로그를 기록하고, 모니터링 시스템을 구축하여 안정성을 확보합니다.

이러한 단계를 통해 자바에서 MCP 도구를 개발하고 LangChain4j 기반의 AI 시스템에 성공적으로 통합할 수 있습니다.

태그: java MCP LangChain4j Stdio HTTP

5월 31일 09:02에 게시됨