[Spring AI] Spring AI 로 MCP Server 구현하기— @McpTool,@McpToolParam
by 우와한개발자
1. MCP Server란
- 외부 도구나 데이터를 MCP 표준 인터페이스로 포장해서 노출하는 서버
1) 일반 서버와의 차이
- 일반 서버는 "어떤 URL로 어떻게 호출할지" 를 개발자가 코드로 직접 작성한다.
- MCP 서버는 "어떤 Tool이 있고 언제 쓰는지" 를 description으로 LLM에게 설명하면 LLM이 알아서 판단해서 호출한다.
| 구분 | 일반 서버 (REST API) | MCP Server |
| 호출 주체 | 사람이 만든 클라이언트 코드 | LLM이 자율적으로 판단해서 호출 |
| 인터페이스 | 엔드포인트 URL + HTTP 메서드 | Tool 이름 + description |
| 호출 방식 | 개발자가 직접 코드로 호출 | LLM이 description 보고 자동 선택·호출 |
| 통신 규격 | REST, GraphQL 등 | JSON-RPC 2.0 |
| 응답 형식 | 자유롭게 설계 | MCP 표준 형식 |
| 주요 목적 | 사람 또는 시스템 간 통신 | LLM에게 능력을 확장시켜주는 도구 |
2) MCP Server의 주요 기능
| 기능 | 역할 | 관련 메서드 |
| Tools | LLM이 능동적으로 호출할 수 있는 함수 | tools/list, tools/call |
| Resources | LLM이 읽을 수 있는 데이터 노출 | resources/list, resources/read |
| Prompts | 재사용 가능한 프롬프트 템플릿 제공 | prompts/list, prompts/get |
(1) Tools
- LLM이 능동적으로 호출할 수 있는 함수 목록
- tools/list로 목록 조회 → tools/call로 실행
- 예: 파일 읽기, DB 조회, 이메일 전송, 검색
(2) Resources
- LLM이 읽을 수 있는 데이터 노출
- URI 형식으로 식별 (file:///path, db://table/id)
- Tools와 차이: Resources는 데이터 조회 전용, Tools는 동작 실행
(3) Prompts
- 재사용 가능한 프롬프트 템플릿 제공
- 서버가 미리 정의해둔 프롬프트를 LLM이 가져다 사용
3) MCP Server 종류
(1) 공식 제공 서버 — npx 한 줄로 바로 사용
- Anthropic 및 커뮤니티에서 이미 만들어둔 서버
- npx : Node.js 패키지를 설치 없이 바로 실행하는 도구.(공식 MCP 서버들이 Node.js 패키지로 배포되어 있어서 npx로 바로 실행)
- 직접 코드 작성 없이 mcp.json에 추가하는 것만으로 바로 사용 가능
- 단, Node.js가 설치되어 있어야 함
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/projects"]
}
}
}
- 파일시스템, DB, Git, GitHub, Slack 등 범용 도구는 공식 서버로 충분
| 서버 | 패키지 | 기능 |
| 파일시스템 | @modelcontextprotocol/server-filesystem | 로컬 파일 읽기/쓰기/탐색 |
| Git | @modelcontextprotocol/server-git | Git 저장소 조회, 커밋 히스토리 |
| GitHub | @modelcontextprotocol/server-github | GitHub API 연동 |
| PostgreSQL | @modelcontextprotocol/server-postgres | DB 조회 |
| Slack | @modelcontextprotocol/server-slack | Slack 메시지 조회·전송 |
| Brave Search | @modelcontextprotocol/server-brave-search | 웹 검색 |
(2) 직접 구현 서버 — 커스텀 비즈니스 로직이 필요할 때
- 공식 서버에 없는 우리 회사/프로젝트 전용 기능이 필요할 때 직접 구현
"우리 회사 주문 DB에서 오늘 주문 현황 조회해줘"
"사내 배포 시스템에 배포 요청 날려줘"
"우리 서비스 회원 정보 조회해줘"
- 이런 건 공식 서버에 없으니까 Spring AI로 직접 MCP 서버를 만들어서 연결
2. Spring AI로 MCP Server 직접 구현
1) 의존성 추가
dependencies {
// Spring Web (MVC)
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring AI MCP Server (WebMVC / SSE)
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
// DevTools
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// Lombok (선택)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
2) application.properties 설정
# MCP Server 설정
spring.ai.mcp.server.protocol=STREAMABLE
spring.ai.mcp.server.annotation-scanner.enabled=true
# Server 포트
server.port=8090
- protocol=STREAMABLE → Streamable HTTP 방식으로 통신. /mcp 단일 엔드포인트 사용. SSE는 deprecated
- annotation-scanner.enabled=true → @McpTool, @McpResource, @McpPrompt 어노테이션을 자동 스캔해서 등록. 이 설정이 없으면 @McpTool을 붙여도 LLM이 인식하지 못함
3) Main 클래스
@SpringBootApplication
@ConfigurationPropertiesScan
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
- @ConfigurationPropertiesScan → application.properties에 정의한 MCP 관련 설정 프로퍼티를 자동으로 스캔해서 바인딩. 이 어노테이션이 없으면 MCP 서버 설정이 제대로 적용되지 않음
4) @McpTool — Tool 구현
- 메서드에 @McpTool을 붙이면 LLM이 호출할 수 있는 Tool로 자동 등록
- @McpToolParam으로 파라미터별 설명과 필수 여부 명시
- description이 LLM이 "언제 이 Tool을 써야 하는지" 판단하는 기준 — 명확하게 작성할수록 정확도가 향상된다.
@Service
public class CalculatorService {
@McpTool(
name = "calculate-sum",
description = "두 정수의 합을 계산합니다. 사용 시점: 덧셈 계산이 필요할 때"
)
public int add(
@McpToolParam(description = "첫 번째 숫자", required = true) int a,
@McpToolParam(description = "두 번째 숫자", required = true) int b
) {
return a + b;
}
}
5) 파일시스템 MCP 서버 구현
@Service
public class FileSystemToolService {
private final Path allowedRoot =
Path.of("/Users/username/projects").toAbsolutePath();
@McpTool(
name = "read-file",
description = "파일 내용을 읽어서 반환합니다. 파라미터: filePath (읽을 파일의 절대경로)"
)
public String readFile(
@McpToolParam(description = "읽을 파일의 절대경로", required = true)
String filePath
) {
try {
Path path = resolveAndValidate(filePath);
return Files.readString(path);
} catch (IOException e) {
return "파일 읽기 실패: " + e.getMessage();
}
}
@McpTool(
name = "write-file",
description = "파일을 생성하거나 내용을 덮어씁니다. 사용 시점: 파일 생성, 코드 저장이 필요할 때"
)
public String writeFile(
@McpToolParam(description = "파일 경로", required = true) String filePath,
@McpToolParam(description = "저장할 내용", required = true) String content
) {
try {
Path path = resolveAndValidate(filePath);
Files.createDirectories(path.getParent());
Files.writeString(path, content);
return "저장 완료: " + filePath;
} catch (IOException e) {
return "파일 쓰기 실패: " + e.getMessage();
}
}
@McpTool(
name = "list-directory",
description = "디렉토리 내 파일 목록을 조회합니다."
)
public String listDirectory(
@McpToolParam(description = "조회할 디렉토리 경로", required = true)
String dirPath
) {
try {
Path path = resolveAndValidate(dirPath);
return Files.list(path)
.map(p -> (Files.isDirectory(p) ? "[DIR] " : "[FILE] ") + p.getFileName())
.collect(Collectors.joining("\\n"));
} catch (IOException e) {
return "디렉토리 조회 실패: " + e.getMessage();
}
}
@McpTool(
name = "search-files",
description = "파일명 패턴으로 파일을 검색합니다. 예: *.java, *.yml"
)
public String searchFiles(
@McpToolParam(description = "검색할 디렉토리 경로", required = true) String dirPath,
@McpToolParam(description = "검색 패턴 (예: *.java)", required = true) String pattern
) {
try {
Path path = resolveAndValidate(dirPath);
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:" + pattern);
return Files.walk(path)
.filter(p -> matcher.matches(p.getFileName()))
.map(Path::toString)
.collect(Collectors.joining("\\n"));
} catch (IOException e) {
return "파일 검색 실패: " + e.getMessage();
}
}
// 허용된 경로 밖 접근 차단 — 보안 필수
private Path resolveAndValidate(String filePath) {
Path resolved = allowedRoot.resolve(filePath).normalize();
if (!resolved.startsWith(allowedRoot)) {
throw new SecurityException(
"접근 거부: 허용 경로(" + allowedRoot + ") 밖입니다."
);
}
return resolved;
}
}
6) @Tool vs @McpTool 비교
| 구분 | @Tool | @McpTool |
| 용도 | Spring AI Tool Calling (LLM과 직접 통신) | MCP Server Tool 노출 |
| 등록 방식 | ToolCallbackProvider Bean 등록 필요 | @Component, @Service만 붙이면 자동 등록 |
| 파라미터 설명 | 별도 지정 어려움 | @McpToolParam으로 파라미터별 상세 설명 가능 |
| 적합한 상황 | Spring AI ChatClient와 함께 사용 | MCP 서버 구현 |
3. 전체 동작 확인하기
1) 서버 실행
./gradlew bootRun
2) 서버 로그에서 Tool 등록 확인
Started McpServerApplication on port 8090
Registered MCP Tool: read-file
Registered MCP Tool: write-file
Registered MCP Tool: list-directory
Registered MCP Tool: search-files
3) Cursor에서 연결 후 Tool 목록 확인
Settings → MCP → my-spring-server → Tools 탭
- read-file, write-file, list-directory, search-files 목록이 보이면 정상
4) 실제 호출 테스트
Cursor 채팅창: "src/main/java 폴더에 Java 파일 목록 보여줘"
↓
LLM → search-files Tool 자동 선택
↓
tools/call { "name": "search-files", "arguments": { "dirPath": "src/main/java", "pattern": "*.java" } }
↓
결과 반환
'AI > Spring AI' 카테고리의 다른 글
블로그의 정보
우와한개발자 님의 블로그
우와한개발자