우와한 개발자

[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" } }
  ↓
결과 반환

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기