The Model Context Protocol (MCP) is a standardized protocol that enables AI models to interact with external tools and resources in a structured way. It supports multiple transport mechanisms to provide flexibility across different environments.
DX(Digital Transformation)에서 AX (AI Transformation) 로 넘어가는 이 변환점에서 나는 어떻게 대응할 수 있을까? 고민하다 보니 자연스럽게 MCP와 Spring AI에 관심을 가질 수밖에 없었다. 또한 이전에 개인프로젝트로 진행하고 싶었던 주제와 매우 연관성이 크다고 생각이 들어 동기부여는 충분했다.
아직 Spring AI는 정식 버전이 없고 1.0.0-SNAPSHOT 버전으로 가이드가 되어있다.
따라서 modelcontextprotocol.io 의 내용과 Spring AI의 공식 문서를 참조하고, 완벽한 코드를 작성하는 것이 아닌 MCP가 제공하는 기능과 흐름에 대해 예제를 통해 이해한다.
Python의 전유물로만 알았던 AI를 Spring, 즉 Java에서 어떻게 해석하고 적용하는지 이해하고 개인 프로젝트에 적용하는 것이 최종 목표이다.
Core Architecture

다양한 아키텍처 그림들이 있어서 핵심 구조만 나타내면 크게 3가지 개념이 있다.
MCP Host
MCP를 통한 데이터 접근을 하려는 프로그램 또는 AI를 뜻한다.
대표적인 예로 MCP Server를 지원하는 Claude Desktop과 Cursor AI가 MCP Host 이다.
MCP Client
일종의 DB 커넥션이다.
MCP Client는 MCP Server와 연결하기 위해 1:1 커넥션을 유지한다.
MCP Server
MCP Server는 특정 Tool
과 Resource
를 AI가 사용할 수 있도록 제공하는 경량 프로그램이다.
코드리뷰 AI 플랫폼을 예로 들면,
사용자 화면과 LLM 상호작용 화면 = MCP Host + MCP Client
repository에서 pr 이력을 가져오고 = Github MCP Server
코드를 분석하며 = CodeRabbit MCP Server
pr에 review를 남기고 = Github MCP Server
Slack에 완료 알림을 보내기 = Slack MCP Server
Concepts
Tools
MCP Server가 제공해주는 Tool
이다. 쉽게 생각하면 기존에 API를 설게할 때 @GetMapping, @PostMapping 등등이 모두 Tool
이 될 수 있다.
@Tool
@Tool(description = "Find person by ID")
public Person findPersonByID(@ToolParam(description = "Person ID") Long id){
return personRepository.findById(id).orElse(null);
}
Spring AI MCP 에서는 @Tool
형태로 도구를 정의한다.
description
은 기본적으로 human-readable 형태로 작성되지만 작성 목적은 AI가 어떤 Tool 인지 알려주기 위함이기 때문에 명확하게 작성해야 한다.
@ToolParam
도 동일하게 필요한 파라미터의 정의를 나타낸다.
언뜻보면 @Controller의 api 주소와 @PathVariable과 유사한 구조이다.
ToolCallbackProvider
MCP Client가 MCP Server로 사용할 수 있는 Tool-List에 대해 조회했을 때 반환해주는 타입이다.
따라서 MCP Server에서 정의되어있는 모든 Tool들을 묶어주는 과정이 필요하다.
@Configuration
public class PersonToolCallbackProvider {
@Bean
public ToolCallbackProvider tools(PersonTools personTools){
return MethodToolCallbackProvider.builder()
.toolObjects(personTools)
.build();
}
}
PersonTools
는 @Service
의 기능과 유사하다. @Service
안에 다양한 비즈니스 로직이 있듯이 PersonTools
안에 여러 @Tool
이 존재한다.
Tool로 등록하고 싶은 클래스가 늘어나면 파라미터 추가하거나, 리스트로 만들어서 넣거나 방법은 다양하다.
Resources
사용자 요청에 대해 AI가 참고할 수 있는 Resource가 있는지 참고할 수 있도록 한다.
Prompts
MCP Server와 MCP Client의 언어 모델과 상호 작용하기 위한 규격을 제공하기 위해 사용한다.
Sampling
MCP Client에서 MCP Server로 도구(Tool)를 요청(Request)하고 MCP Server가 수행한 뒤 결과를 Client로 응답(Response)하는 일련의 과정을 뜻한다.
MCP example
Spring MCP Client에 외부 MCP Server 연동하기
# mcp-servers-config.json
{
"mcpServers": {
"github": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"@smithery/cli@latest",
"run",
"@dev-assistant-ai/github",
"--key",
"{ACCESS_TOKEN}"
]
},
"mcp-discord": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"@smithery/cli@latest",
"run",
"@barryyip0625/mcp-discord",
"--key",
"{ACCESS_TOKEN}"
]
}
}
}
Smithery , MCP Market 같은 여러 MCP market place에서 사용하고 싶은 api의 key, token과 smithery 계정이 있다면 연결을 위한 json 형식의 텍스트 위와 같이 생성해준다.
spring:
ai:
mcp:
client:
enabled: true
name: simple-test-client
version: 1.0.0
type: sync
request-timeout: 20s
stdio:
servers-configuration: classpath:/mcp-servers-config.json
root-change-notification: true
anthropic:
api-key: {ACCESS_KEY}
chat:
options:
model: claude-3-haiku-20240307
그리고 spring.ai.mcp.client.stdio.servers-configuration:
에 MCP Server 설정 파일의 경로를 지정해주면 Client와 Server의 연결이 끝났다.
spring.ai.anthropic~
은 Anthropic의 Claude 모델을 사용하기 때문이고 다른 모델을 사용하고 싶다면 공식문서를 참고하자.
@Configuration
public class ChatbotConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
List<McpSyncClient> mcpSyncClients) {
return chatClientBuilder
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
.defaultAdvisors(new MessageChatMemoryAdvisor(
new InMemoryChatMemory()))
.build();
}
}
이제 ChatClient.Builder
를 통해 LLM의 기본 스펙을 지정한다.
@Component
@RequiredArgsConstructor
public class ChatbotRunner implements CommandLineRunner {
private final ChatClient chatClient;
@Override
public void run(String... args) {
System.out.println("\nI am your AI assistant.\n");
try(Scanner sc = new Scanner(System.in)){
while(true){
try{
System.out.print("\n사용자: ");
System.out.println("\nAI:
" + chatClient.prompt(sc.nextLine()).call().content());
}catch (Exception e){
e.printStackTrace();
System.out.println("요류 발생");
}
}
}
}
}
Scanner
를 통해 입력받은 문자를 chatClient.prompt(sc.nextLine)
를 해서 담고 .call()
한 결과를 .content()
통해 꺼내서 출력하면 간단하게 테스트 해볼 수 있다.

동일한 입력을 Claude 3.7 최신 모델로 검색하면 확연히 차이를 알 수 있다.

Spring MCP Client에 커스텀 Tool 추가하기
@Configuration
public class ChatbotConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
List<McpSyncClient> mcpSyncClients,
WeatherTool weatherTool,
CodeReviewTool codeReviewTool)
{
ToolCallbackProvider weatherTools = MethodToolCallbackProvider.builder()
.toolObjects(weatherTool)
.build();
ToolCallbackProvider codeReviewTools = MethodToolCallbackProvider.builder()
.toolObjects(codeReviewTool)
.build();
return chatClientBuilder
.defaultSystem("너는 깃허브,디스코드,날씨,코드리뷰 요청을 수행할 수 있어.")
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients),
weatherTools,
codeReviewTools)
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
.build();
}
}
기존의 ChatbotConfig
에서 사용할 Tools를 정의한 객체를 defaultTools()
에 넣어주면 된다.
Builder defaultTools(String... toolNames);
Builder defaultTools(ToolCallback... toolCallbacks);
Builder defaultTools(List<ToolCallback> toolCallbacks);
Builder defaultTools(Object... toolObjects);
Builder defaultTools(ToolCallbackProvider... toolCallbackProviders);
1.0.0-SNAPSHOT 버전에서는 다양한 형식을 지원하고 있다.
Spring MCP Client에 Spring MCP Server 연동하기
1.0.0-SNAPSHOT
기준 다음과 같은 의존성이 존재한다.
STDIO
StandardInputOutput, 표준입출력 방식으로 로컬 환경 또는 터미널 기반 모델과 연동하기 위해 사용
# MCP Client
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
# MCP Server
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
SSE
ServerSentEvent, WebSocket과 상반되는 단방향 통신으로 일반적인 MCP Server의 통신 방식
# MCP Client
# mcp-client는 stdio, sse 모두 지원한다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
# MCP Server
# 공식문서에서 배포 시에는 WebFlux 기반 SSE 연결을 권장한다
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
Spring MCP Client
spring:
ai:
mcp:
client:
enabled: true
name: simple-test-client
version: 1.0.0
type: sync
request-timeout: 20s
sse:
connections:
person-mcp-server:
url: http://localhost:8060
root-change-notification: true
toolcallback:
enabled: true
기존 설정에서 spring.ai.mcp.client.sse.connections.{MCP SERVER NAME}.url:
과 spring.ai.mcp.client.toolcallback.enabled: true
가 추가 되었다.
이제 client는 Spring MCP Server와 SSE 통신을 하고 Server로 부터 tool
을 받기 때문에 toolcallback
을 할 수 있도록 설정을 해준다.
Spring MCP Server
@RestController
@RequestMapping("/persons")
public class PersonController {
private final ChatClient chatclient;
public PersonController(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider tools){
this.chatclient = chatClientBuilder.defaultTools(tools).build();
}
@GetMapping("/count-by-nationality/{nationality}")
public String countByNationality(@PathVariable String nationality){
PromptTemplate pt = new PromptTemplate("""
Person의 Nationality가 {nationality}인 Person의 Name과 age를 알려줘
""");
Prompt p = pt.create(Map.of("nationality", nationality));
return this.chatclient.prompt(p)
.call()
.content();
}
@GetMapping("/")
public String inputRandomPersonData(){
PromptTemplate pt = new PromptTemplate("""
Person 데이터를 인메모리에 저장하기
""");
Prompt p = pt.create();
String result = this.chatclient.prompt(p).call().content();
System.out.println("result = " + result);
return result;
}
}
위와 같은 Controller만 작성하면 MCP Client는 완성된다. 이제 MCP Server를 만들어보자.
spring:
ai:
mcp:
server:
name: person-mcp-server
version: 1.0.0
jpa:
database-platform: H2
generate-ddl: true
hibernate:
ddl-auto: create-drop
logging.level.org.springframework.ai: DEBUG
server.port: 8060
로컬에서 MCP Client를 8080포트에 띄우기 때문에 MCP Server는 8060포트로 지정한다.
테스트를 위해 spring-data-jpa를 사용해 인메모리에 저장된 값을 ai가 조회할 수 있는 tool을 만든다.
@Service
@RequiredArgsConstructor
public class PersonTools {
private final PersonRepository personRepository;
@Tool(description = "Find person by ID")
public Person findPersonByID(@ToolParam(description = "Person ID") Long id){
return personRepository.findById(id).orElse(null);
}
@Tool(description = "Find all persons by nationality")
public List<Person> findPersonByNationality(
@ToolParam(description = "Nationality") String nationality){
return personRepository.findByNationality(nationality);
}
@Tool(description = "Person 데이터를 인메모리에 저장하기")
public void generateRandomDataPerson(){
for (int i=0; i<8; i++){
personRepository.save(new Person(NAME[i],AGE[i],NATIONALITY[i]));
}
}
}
/***********************************************************************************/
@Configuration
public class PersonToolCallbackProvider {
@Bean
public ToolCallbackProvider tools(PersonTools personTools){
return MethodToolCallbackProvider.builder()
.toolObjects(personTools)
.build();
}
}
동작결과
[GET] localhost:8080/persons/ 로 요청을 보내면

동작 결과를 분석해보면 1개의 요청에 대해 N개의 tool을 사용한 것을 알 수 있다. 또한 tool의 함수명도 참조한 것을 알 수 있다.

Client와 Server가 먼저 연결이 되면서 Client는 사용가능한 tools/list를 요청한다.
이후에 llm을 통해 요청이 들어오면 보유한 tool 중 요청에 적합한 tool을 AI가 직접 판단하여 Server로 해당 tool에 대해 응답을 요청한다.
따라서 요청에 대한 정확도를 높이기 위해서 명확한 description과 prompt가 중요하다.
Next
Github MCP Server A to Z 구현