Model Context Protocol
Model Context Protocol (MCP)
LangChain4j는 MCP(Model Context Protocol) 를 지원해요. 도구를 제공하고 실행할 수 있는 MCP 호환 서버와 통신하는 프로토콜이죠. 프로토콜에 대한 일반 정보는 MCP 웹사이트에서 볼 수 있어요.
MCP란
MCP는 두 가지 전송(transport) 타입을 지정하는데, 둘 다 지원돼요:
- Streamable HTTP — 클라이언트가 HTTP 요청을 보내면 서버는 일반 응답으로 또는 시간에 따라 여러 응답을 보내야 한다면 SSE 스트림을 열어서 응답해요.
- stdio — 클라이언트가 MCP 서버를 로컬 하위 프로세스로 실행하고 표준 입출력으로 직접 통신해요.
사양 위에 LangChain4j는 WebSocket 전송도 구현해요. 이 전송은 표준화되어 있지 않고, 현재 Quarkus MCP Server 확장이 구현한 WebSocket 전송과 호환되도록 클라이언트 쪽을 개발했어요. WebSocket을 노출하지만 다른 프레임워크로 빌드된 MCP 서버와의 호환성은 보장되지 않아요.
추가로 LangChain4j는 컨테이너 이미지로 배포된 stdio MCP 서버를 쓸 수 있는 Docker stdio 전송을 지원해요.
채팅 모델 또는 AI service가 MCP 서버가 제공하는 도구를 실행하게 하려면, MCP 도구 제공자(tool provider) 인스턴스를 만들어야 해요.
MCP 도구 제공자 만들기
MCP 전송
먼저 MCP 전송 인스턴스가 필요해요.
stdio의 경우 — NPM 패키지에서 하위 프로세스로 서버를 시작하는 예:
McpTransport transport = StdioMcpTransport.builder()
.command(List.of("/usr/bin/npm", "exec", "@modelcontextprotocol/[email protected]"))
.logEvents(true) // only if you want to see the traffic in the log
.build();
Streamable HTTP 전송의 경우 서버의 POST 엔드포인트 URL을 제공해야 해요:
McpTransport transport = StreamableHttpMcpTransport.builder()
.url("http://localhost:3001/mcp")
.logRequests(true) // if you want to see the traffic in the log
.logResponses(true)
.build();
WebSocket 전송의 경우:
McpTransport transport = WebSocketMcpTransport.builder()
.url("ws://localhost:3001/mcp/ws")
.logResponses(true)
.logRequests(true)
.build();
Docker stdio 전송의 경우 먼저 pom.xml에 모듈을 추가해야 해요:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp-docker</artifactId>
</dependency>
그다음 Docker 전송을 만들어요:
McpTransport transport = DockerMcpTransport.builder()
.image("mcp/time")
.dockerHost("unix:///var/run/docker.sock")
.logEvents(true) // if you want to see the traffic in the log
.build();
MCP 클라이언트
전송에서 MCP 클라이언트를 만들어요:
McpClient mcpClient = DefaultMcpClient.builder()
.key("MyMCPClient")
.transport(transport)
.build();
클라이언트 키는 선택이지만, 특히 MCP 클라이언트가 여러 개일 때 설정을 권장해요. 서로를 구분할 필요가 있으니까요.
프로토콜 버전
MCP 클라이언트는 레거시 프로토콜(2025-11-25)과 현대 무상태 프로토콜(2026-07-28)을 모두 지원해요. 기본적으로 클라이언트는 서버의 프로토콜 버전을 자동 감지하며, 서버가 지원하면 2026-07-28을 선호해요:
// Auto-detect (default behavior)
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.build();
// Force modern protocol
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.protocolVersion("2026-07-28")
.build();
// Force legacy protocol
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.protocolVersion("2025-11-25")
.build();
감지에는 클라이언트 시작 시 왕복이 하나 더 들어요: server/discover 요청을 보내고, 응답이 오류거나 protocolDetectionTimeout 안에 도착하지 않으면 서버를 레거시로 취급해요. 이 타임아웃 기본값은 initializationTimeout(30초)인데, 하위 프로세스로 시작된 서버는 아무것도 답하기 전에 부팅 시간이 필요하기 때문이에요. 너무 일찍 포기하면 현대 서버가 레거시처럼 보일 수 있죠.
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.protocolDetectionTimeout(Duration.ofSeconds(5)) // servers you know answer quickly
.build();
protocolVersion을 명시적으로 설정하면 감지를 완전히 건너뛰어요. 서버가 말하는 프로토콜 버전을 이미 아는 경우에 할 만한 일이에요. 또 서버가 인식하지 못하는 메서드를 받았을 때 나쁘게 반응하는 경우의 탈출구이기도 해요 — 일부 오래된 MCP 서버 구현은 알 수 없는 요청에 오류로 답하는 대신 종료하니까요.
MCP 도구 제공자
마지막으로 클라이언트에서 MCP 도구 제공자를 만들어요:
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.build();
하나의 MCP 도구 제공자로 여러 클라이언트를 동시에 쓸 수 있어요. 이 경우 특정 서버에서 도구를 가져오는 데 실패했을 때 도구 제공자의 동작을 지정할 수도 있어요 — builder.failIfOneServerFails(boolean) 메서드로요. 기본값은 false라 한 서버의 오류를 무시하고 다른 서버로 계속해요. true로 설정하면 어떤 서버의 실패든 도구 제공자가 예외를 던지게 해요.
또 MCP 서버는 수십 개의 도구를 제공할 수 있는데, 주어진 AI service는 그중 몇 개만 필요할 수 있어요. 원치 않는 도구 사용을 막고 환각 가능성을 줄이기 위해서죠. McpToolProvider는 이름으로 이 도구들을 필터링할 수 있어요:
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.filterToolNames("get_issue", "get_issue_comments", "list_issues")
.build();
이렇게 하면 이 ToolProvider로 구성된 AI service가 언급한 3개 도구만 쓸 수 있고, 기존 이슈를 읽을 수 있지만 새로 만들지는 못해요. 더 일반적으로, ToolProvider는 BiPredicate<McpClient, ToolSpecification>으로 도구를 필터링할 수 있어요. 여러 MCP 클라이언트가 같은 이름(그래서 충돌하는 이름)의 도구를 노출할 때도 유용해요:
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient1, mcpClient2)
.filter((mcpClient, tool) ->
!tool.name().startsWith("echoInteger") ||
mcpClient.key().equals("numeric-mcp"))
.build();
도구 제공자를 AI service에 바인딩하려면 AI service 빌더의 toolProvider 메서드를 쓰면 돼요:
Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();
또는 Map<ToolSpecification, ToolExecutor>로 도구를 제공할 수도 있어요:
Map<ToolSpecification, ToolExecutor> tools = mcpClient.listTools().stream().collect(Collectors.toMap(
tool -> tool,
tool -> new McpToolExecutor(mcpClient)
));
AI service에 도구를 바인딩하려면 AI service 빌더의 tools 메서드를 쓰면 돼요:
Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.tools(tools)
.build();
MCP 도구 이름 매핑
여러 MCP 서버를 쓰는데 충돌하는 이름의 도구를 노출하거나(또는 부적절하게 지어진 이름을 조정하고 싶다면) 도구 이름 매핑 함수를 적용하는 게 유용할 수 있어요. McpToolProvider를 만들 때 BiFunction<McpClient, ToolSpecification, String>을 지정해서 할 수 있어요. 예:
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient1, mcpClient2)
.toolNameMapper((client, toolSpec) -> {
// Prefix all tool names with the name of the MCP client and an underscore
return client.key() + "_" + toolSpec.name();
})
.build();
이 후 도구 제공자가 반환한 ToolSpecification 객체는 매핑된(논리적) 이름을 담고, 생성된 ToolExecutor 객체는 도구 호출 시 원래(물리적) 이름을 서버에 전달하도록 고정돼요.
MCP 도구 명세 매핑
MCP 도구 매핑과 비슷하게, 완전한 ToolSpecification도 매핑할 수 있어요:
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.toolSpecificationMapper((client, toolSpec) -> {
// Prefix all tool names with "myprefix_" and convert the description to uppercase
return toolSpec.toBuilder()
.name("myprefix_" + toolSpec.name())
.description(toolSpec.description().toUpperCase())
.build();
})
.build();
AI Services 없이 MCP 사용
지금까지 예제는 고수준 AI Services API로 MCP를 쓰는 법을 보여줬어요. 하지만 저수준 API로 MCP를 쓸 수도 있어요. 빌드한 DefaultMcpClient 인스턴스를 직접 써서 서버에 명령을 실행할 수 있죠:
// obtain a list of tools from the server
List<ToolSpecification> toolSpecifications = mcpClient.listTools();
// build and execute a ChatRequest that has access to the MCP tools
ChatRequest chatRequest = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = chatModel.chat(chatRequest);
AiMessage aiMessage = response.aiMessage();
// if the LLM requested to invoke a tool, forward it to the MCP server
if(aiMessage.hasToolExecutionRequests()) {
for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) {
String resultString = mcpClient.executeTool(req);
// prepare the result for adding it to the memory for the next ChatRequest...
ToolExecutionResultMessage resultMessage = ToolExecutionResultMessage.from(req.id(), req.name(), resultString);
}
}
도구 캐싱에 관한 참고
DefaultMcpClient는 MCP 도구의 내부 캐시를 유지해요. 한 번 가져오면 서버가 목록이 갱신됐다는 알림을 보내지 않는 한 MCP 서버에 도구 목록을 다시 요청하지 않아요. DefaultMcpClient.evictToolListCache()를 호출해서 이 캐시를 수동으로 비울 수 있어요. 캐싱을 완전히 비활성화하고 싶다면 클라이언트를 다음과 같이 구성하세요:
McpClient mcpClient = new DefaultMcpClient.Builder()
.key("MyMCPClient")
.transport(transport)
.cacheToolList(false)
.build();
MCP Registry 클라이언트
LangChain4j는 MCP 레지스트리와 대화할 수 있는 별도 클라이언트 구현도 제공해요. 지금은 읽기 전용 작업만 구현되어 있어요(MCP 서버를 검색할 수 있지만, 서버를 추가·관리하는 건 지원하지 않아요 — 그건 공식 도구를 쓰세요).
:::warning MCP 서버를 발견하고 사용하는 것(특히 로컬에서 실행)은 심각한 보안 위험이 될 수 있어요. 공개 레지스트리에서 찾은 MCP 서버를 실행하기 전에 신뢰할 수 있는지 반드시 확인하세요. :::
레지스트리 클라이언트는 dev.langchain4j.mcp.registryclient 패키지에 있고, 다음과 같이 초기화할 수 있어요:
McpRegistryClient client = DefaultMcpRegistryClient.builder()
.baseUrl("URL-OF-THE-REGISTRY")
.build();
base URL을 제공하지 않으면 공식 레지스트리가 기본값으로 쓰여요(https://registry.modelcontextprotocol.io). 그다음 MCP 서버를 검색하려면 registry.listServers(McpServerListRequest) 메서드를 써요.