이 시리즈는 Backend API의 Swagger 문서를 MCP 서버로 제공하여, LLM에서 자연어로 API를 검색하고, 완전한 네트워킹 코드를 자동 생성하는 시스템을 만드는 과정입니다.
1편 : LLM 기반 API 탐색 자동화 설계
2편 : Swagger API 파싱
3편 : DB 저장 설계와 트러블 슈팅
4편: Spring AI로 MCP 서버를 구축하고, Claude Code 연동하기 (현재 글)
모든 코드는 해당 레포지토리에서 확인할 수 있습니다.
GitHub - WooJJam/swagger-mcp-server: Spring AI를 사용하여 Swagger API를 LLM이 자연어로 조회할 수 있도록 제공
Spring AI를 사용하여 Swagger API를 LLM이 자연어로 조회할 수 있도록 제공하는 MCP Server - WooJJam/swagger-mcp-server
github.com
Introduce
3편에서는 파싱한 스키마를 DB에 저장하는 설계를 다루었다. $ref를 재귀적으로 resolve해서 중첩 구조까지 완전히 펼친 상태로 저장하고, FieldInfo에 properties와 items 필드를 추가하여 트리 구조를 그대로 표현할 수 있게 하였다. 이로써 완전한 구조로 데이터베이스에 저장되어 있다. $ref는 하나도 남아있지 않고, required 정보는 모든 깊이의 필드에 인라인됐으며, SchemaSupporter가 이를 Map 구조로 변환할 준비까지 완료되었다.
남은 과제는 한 가지다.
Backend API의 Swagger 문서를 MCP 서버로 제공하여, 클라이언트 개발자가 AI를 통해 자연어로 API를 검색하고 완전한 네트워킹 코드를 자동 생성할 수 있도록 하는 구조를 완성하는 것이다.
What is MCP?

MCP(Model Context Protocol)는 Anthropic에서 발표한 표준 프로토콜로 AI 모델이 외부 서비스와 통신하기 위한 규격화된 프로토콜이다. 이는 USB-C 포트를 생각하면 된다. 노트북이나 전자 제품들의 제조사가 다르더라도 USB-C 규격만으로 같은 케이블로 연결할 수 있듯이 MCP를 지원하는 AI 모델이라면 어떤 MCP 서버든 동일한 방식으로 연결할 수 있다.
📌 Participants
MCP 아키텍처에는 3가지 역할이 있다.
- MCP Host
- 개발자가 직접 인터랙션하는 인터페이스
- Claude Code, ChatGPT 와 같은 AI 애플리케이션
- MCP Client
- MCP Host와 MCP Server 사이의 중간 레이어
- MCP Server와 연결을 유지하고, MCP Server의 컨텍스트 정보를 제공
- MCP Server
- MCP 클라이언트에 컨택스트를 제공하는 프로그램
우리 프로젝트에서는 Cluade Code가 MCP Host가 되고, Claude Code 내부에 MCP Client를 가지고 있으며 swagger-mcp-server가 MCP Server가 된다.
📌 Layer
MCP는 내부적으로 Transport와 Data 두 계층으로 나뉜다.
1. Transport Layer
Transport Layer는 실제 통신 채널로 두 가지 방식이 있다.
- stdio
- 표준 입출력으로 통신
- 로컬 프로세스간 직접 통신에 적합
- 네트워크 오버헤드가 없어 성능 최적화
- Streamable HTTP
- 원격 서버 통신에 사용
- HTTP POST로 요청을 보내고, 스트리밍 응답에는 SSE 사용이 가능
해당 프로젝트는 팀 전체가 원격 서버에서 함께 사용하는 구조이므로 Streamable HTTP를 선택하였다.
2. Data Layer
Data Layer는 어떤 데이터와 명령을 주고 받을지 규정하는 계층으로 JSON-RPC 2.0 기반으로 교환 프로토콜이 구현되어 있다. 여기서 JSON-RPC 2.0은 전송 방식이 아니라 메시지 포맷이자 통신 규약이다. stdio와 streamable HTTP 위에서 JSON-RPC 2.0 형식의 메시지를 주고받는 것이다. 또한 MCP의 `라이프사이클 관리(Initialize -> Operation -> Shutdown)`, `primitives(tools, resources, prompts)` 같은 핵심 기능이 해당 계층에서 처리된다.
- JSON-RPC 2.0 메시지
// 요청 (id 있음, 응답을 기다림)
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "searchApiByKeyword",
"arguments": { "keyword": "login" }
},
"id": 1
}
// 응답
{
"jsonrpc": "2.0",
"result": {
"content": [{ "type": "text", "text": "[{\"id\": 12, \"path\": \"/api/auth/login\"}]" }]
},
"id": 1
}
// 알림 (id 없음, 응답 불필요)
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed",
"params": {}
}
📌 MCP Tool의 동작 흐름
MCP Server 제공하는 핵심 기능이 Tool이다.
- 탐색
- `tools/list` 메서드로 이 MCP Server에 어떤 Tool이 정의되어 있는지 목록을 요청
- MCP Server는 Tool 이름, 설명 등을 반환
- 선택
- AI는 반환된 Tool 중 적합하다고 판단하는 Tool 선택
- 실행
- `tools/call` 메서드로 선택한 Tool 호출
- 변경 알림
- Tool 목록이 바뀌면 `notifications/tools/list_changed` 알림으로 클라이언트에게 전달
이러한 흐름들을 직접 구현하면 `tools/list` 핸들러, `tools/call` 핸들러, 라이프사이클, JSON-RPC 2.0 메시지 파싱 및 직렬화, Transport 계층 처리까지 상당한 보일러플레이트가 필요하다.
하지만 Spring AI를 사용하면 간편하게 구축할 수 있다.
How to Implement an MCP Server with Spring AI
MCP를 직접 구현하기 위해서는
@PostMapping("/mcp")
public McpResponse handleMcpRequest(@RequestBody McpRequest request) {
return switch (request.getMethod()) {
case "initialize" -> handleInitialize(request);
case "tools/list" -> McpResponse.builder()
.id(request.getId())
.result(Map.of(
"tools", List.of(
Map.of(
"name", "searchApiByKeyword",
"description", "Search API endpoints by keyword",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of(
"keyword", Map.of("type", "string")
),
"required", List.of("keyword")
)
)
// 나머지 Tool도 동일하게...
)
))
.build();
case "tools/call" -> handleToolCall(request);
// ...
};
}
이렇게 귀찮고, 복잡한 과정을 반복해야한다.
하지만 Spring AI를 사용하면 단순히 의존성만 추가하면 된다.
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.2'
implementation 'org.springframework.ai:spring-ai-mcp-annotations:1.1.2'
Introduction :: Spring AI Reference
Support for all major Vector Database providers such as Apache Cassandra, Azure Cosmos DB, Azure Vector Search, Chroma, Elasticsearch, GemFire, MariaDB, Milvus, MongoDB Atlas, Neo4j, OpenSearch, Oracle, PostgreSQL/PGVector, Pinecone, Qdrant, Redis, SAP Han
docs.spring.io
그리고 yml파일을 통해 앞서 언급한 세부 내용을 설정하면 끝이다.
spring:
ai:
mcp:
server:
name: swagger-mcp-server
version: 0.0.1
type: SYNC
protocol: STREAMABLE
capabilities:
tool: true
resource: false
prompt: false
completion: false
- protocol : STREAMABLE
- 전송 계층에서 사용할 프로토콜로 Streamable HTTP 방식 채택
- type : SYNC
- Tool 호출을 동기적으로 처리
- 비동기 스트리밍이 필요하면 ASYNC를 사용
- 해당 프로젝트에서는 DB 조회 후 응답을 반환하는 단순한 흐름이므로 SYNC 채택
capabilities에는 tool만 허용하였다. 현재 우리 시스템에서는 Resources나 Prompts는 불필요하였기 때문에 허용하지 않았다.
이처럼 직접 구현하였으면 설정에만 수십~수백줄이었을 인프라 코드가 단 몇줄로 끝이났다.
📌 MCP Tool 정의
MCP Tool 목록을 설계하기 이전에 AI가 어떤 순서로 API 정보를 탐색하는가를 생각해보면:
- 어떤 API가 있는지 찾기 (`searchApiByKeyword`)
- 특정 API의 전체 정보 가져오기 (`getApiDetail`)
- Reqeust DTO 정보 가져오기 (`getRequestFormat`)
- Response DTO 정보 가져오기 (`getResponseFormat`)
- 에러 코드 가져오기 (`getErrorFormats`)
3~5는 getApiDetail의 하위 집합이지만 별도로 분리해주었다. 이는 복잡한 API를 다룰 때 AI의 컨텍스트를 효율적으로 관리하기 위해서이다. Request DTO만 필요한 데 Error 응답까지 실어 보내주는 것은 불필요한 정보이다. 따라서 딱 필요한 정보만 서빙하는 것이 좋다고 판단하였다.
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
@Slf4j
@Component
@RequiredArgsConstructor
public class SwaggerMcpTools {
private final ApiSearchService apiSearchService;
@McpTool(description = "Search API endpoints by keyword. Searches in path, summary, description, and operationId.")
public List<ApiSummary> searchApiByKeyword(
@McpToolParam(description = "Search keyword", required = true) final String keyword
) {
log.info("[MCP Tool] searchApiByKeyword 호출: {}", keyword);
return apiSearchService.searchApiByKeyword(keyword);
}
@McpTool(description = "Get detailed API information including request body, parameters, responses, and errors. Returns AI-friendly format.")
public ApiDetailForAI getApiDetail(
@McpToolParam(description = "API endpoint ID", required = true) final Long apiId
) {
log.info("[MCP Tool] getApiDetail 호출: {}", apiId);
return apiSearchService.getApiDetail(apiId);
}
@McpTool(description = "Get request format for API endpoint including body schema and parameters.")
public RequestForAI getRequestFormat(
@McpToolParam(description = "API endpoint ID", required = true) final Long apiId
) {
log.info("[MCP Tool] getRequestFormat 호출: {}", apiId);
return apiSearchService.getRequestFormat(apiId);
}
@McpTool(description = "Get success response formats for API endpoint by status code.")
public Map<Integer, ResponseForAI> getResponseFormat(
@McpToolParam(description = "API endpoint ID", required = true) final Long apiId
) {
log.info("[MCP Tool] getResponseFormat 호출: {}", apiId);
return apiSearchService.getResponseFormat(apiId);
}
@McpTool(description = "Get error response formats for API endpoint by status code. Includes error codes and messages.")
public Map<Integer, ErrorForAI> getErrorFormats(
@McpToolParam(description = "API endpoint ID", required = true) final Long apiId
) {
log.info("[MCP Tool] getErrorFormats 호출: {}", apiId);
return apiSearchService.getErrorFormats(apiId);
}
}
Spring AI가 `@McpTool`이 달린 클래스를 애플리케이션 시작 시 스캔해서 자동으로 Tool로 등록한다. 별도 등록 코드는 필요없다.
`@McpTool`을 정의할 때 description이 중요한데 AI가 해당 설명을 읽고, 어느 Tool을 호출할지를 결정한다. "에러 코드 알려줘"와 같은 요청에 getErrorFormats가 선택될 수 있도록, 해당 description에 error codes를 명시적으로 작성해주었다. 만약 description을 대충 쓰게되면 Tool이 있어도 적절한 타이밍에 호출되지 않는다.
📌 최종 응답 예시
AI에게 전달되는 실제 응답 예시는 아래와 같다.
{
"id": 12,
"path": "/api/auth/login",
"method": "POST",
"summary": "로그인",
"request": {
"body": {
"email": { "type": "string", "format": "email", "required": true },
"password": { "type": "string", "format": "password", "required": true },
"rememberMe": { "type": "boolean", "required": false }
},
"parameters": []
},
"responses": {
"200": {
"body": {
"accessToken": { "type": "string", "required": true },
"refreshToken": { "type": "string", "required": true },
"expiresIn": { "type": "integer", "required": true }
}
}
},
"errors": {
"401": {
"code": "AUTH-CREDENTIAL-INVALID",
"message": "아이디 또는 비밀번호가 올바르지 않습니다"
}
}
}
3편에서 언급한 `enrichSchemaWithRequired()`로 모든 필드에 인라인한 required 필드가 여기에서 사용된다.
Testing MCP with Claude Code
이제 모든 준비가 끝났고, 실제 AI와 MCP server를 연동하여보자. 나는 Cluade Code를 이용하여 MCP Server를 등록할 것이다.
claude mcp add --transport http swagger-mcp http://server-ip:port/mcp \
--header "Authorization: Bearer your-token-here" \
--scope project
swagger mcp server는 팀 내부적으로 사용하기 위해서 만든 MCP이다. 따라서 전역으로 설정하지말고, `--scope project`를 통해 현재 프로젝트에만 해당 MCP 서버를 등록하여야 한다. 다른 프로젝트에서는 해당 MCP를 사용해서는 안된다.
MCP를 정상적으로 등록하였다면 claude code를 열고 /mcp를 통해 연결된 MCP 서버와 사용 가능한 Tool 목록을 확인할 수 있다.

📌 시나리오 1) 로그인 API DTO 생성
로그인 API 명세서를 찾아서 Kotlin으로 Request/Response DTO를 생성해줘.
해당 프롬포트를 실행하면 claude code는 연동된 swagger mcp를 통해 사용할 tool을 선택한 뒤, 메서드를 실행하게 된다.

먼저 searchApiByKeyword를 통해 로그인 API 를 조회한다. 그리고 getApiDetail을 통해 API Spec을 가져오고 있다.

최종 결과를 보면 내가 원했던대로 claude code가 로그인 API 명세서를 찾아서 스스로 request, response dto를 생성해주는 것을 확인할 수 있다.
📌 시나리오 2) required 여부 확인
이전에 계속해서 언급하였듯이 필드별 required 여부를 여러 depth에 걸쳐 인라인으로 지정해주었다. 개발중인 서비스 내에 여행 영상 속 동선을 조회하는 API가 있는데 해당 스키마에는 여행 장소에 대한 정보들이 들어있다. 해당 스키마에는 상당히 많은 nullable 필드가 많기 때문에 해당 API로 테스트 해보자.
여행 템플릿 일정 조회 API 명세서를 찾아서 kotlin으로 Request, Response DTO를 생성해줘

이전과 동일하게 searchApiByKeyword -> getApiDetail 순서로 동작하는 것을 확인할 수 있다.

최종 생성된 dto를 보면 request의 query도 적절하게 생성하고, response의 nullable도 정상적으로 작성한 것을 확인할 수 있다.
📌 시나리오 3) 에러 핸들링 코드 생성
로그인 API 에러 코드 목록이랑 핸들러 코드 작성해줘
다음으로는 에러 코드를 조회하고, 핸들링하는 코드를 작성해보는 것을 테스트해보자.

생각했던 것 보다 더 알잘딱깔센으로 잘 찾고, 코드도 잘 작성해준다..!
📌 시나리오 4 - 연동 코드 작성
회원가입 API를 찾아보고, 연동 코드까지 작성해줘.
이제 마지막 시나리오이다.
지금까지는 request, response와 에러 핸들러 코드를 정상적으로 작성하는 것을 테스트하였다. 마지막은 궁극적으로 이를 종합하여 최종 연동 코드까지 claude code가 직접 작성하는 것이 목표다.

최종 결과를 보면 request, response dto도 잘 생성했고, 에러 코드도 확인 한 뒤 정상적으로 핸들링하는 코드를 작성하였다. 그리고 최종 목표였던 연동 코드까지 모두 직접 작성해주는 것을 확인할 수 있다!
테스트 환경은 별도 라이브러리나 프레임워크 없이 순수 Kotlin 코드로만 구성했기 때문에 코드 완성도는 높지 않다. 하지만 이 테스트의 목적은 "연동 코드를 만들어낼 수 있는가"였으므로 코드 디테일은 중요하지 않다.
만약 클라이언트 프로젝트의 컨벤션과 아키텍처를 AI가 파악한 상태에서 연동 코드를 요청하면 그에 맞는 완전한 코드가 나올 것이다.
Wrapping Up
총 4편으로 swagger mcp server를 만드는 과정을 완성하였다.
"로그인 API 찾아서 DTO 만들어주고, 연동 코드를 작성해줘" 와 같은 자연어를 통해 AI가 swagger mcp를 통해 완전한 DTO를 생성하고, 연동 코드까지 완성하는 것을 확인하였다.
Spring AI가 MCP 구현 비용을 극적으로 낮춰주었다. MCP를 직접 구현하면 JSON-RPC 2.0 부터 `tools/list`, `tools/call`, Initialization, Transport 계층 처리까지 직접 인프라 코드를 작성해야한다. 하지만 Spring AI는 적절한 어노테이션과 yml을 통한 설정을 통해 이 전부를 처리할 수 있었다. 해당 프로젝트를 구현하면서 90%의 시간과 노력을 데이터 파싱과 가공에 시간을 사용하였다. 하지만 만약 Spring AI가 없었다면.. 90%를 MCP 자체 구축에 사용했을 것이다.
솔직히 처음 만들기 시작하였을 때는 반신반의했다. 내가 원하는 수준의 퀄리티를 뽑아낼 수 있을 것인지? 적절히 타협하면서 기존 계획을 수정하는 것은 아닐지? 걱정이 됐었다. 그리고 해당 MCP로 정말 팀에게 유용할지도 몰랐다. 하지만 막상 적용해보니 생각보다 퀄리티도 너무 좋았고, API 연동 시 DTO를 직접 손으로 옮겨 적던 작업이 거의 사라졌고, 에러 코드 핸들링 누락 같은 실수도 줄어들었다. 작은 도구와 아이디어 하나가 작업 흐름을 꽤 바꿔놓았다.
이러한 경험이 스스로 자신감을 얻은 계기가 됐다. 단순히 코드를 빨리 작성하는 것 이상으로, AI를 적극 활용하여 팀 개발 환경과 문화에 녹이는 것 자체에 흥미도 생겼다. 앞으로도 반복적이고, 불필요한 작업들을 개선하여 병목을 줄이고, 개발자가 더 중요한 문제에 집중할 수 있는 팀 도구를 계속해서 만들어보고 싶다.
다만 한 가지 남은 기술 부채가 있는데, 현재 인프라가 수동으로 구성되어 있어 Terraform 같은 IaC 도구를 적용하지 못한 것이다. EC2 설정이나 보안 그룹등이 코드로 관리되고 있지 않다. 추후 꼭 인프라 관리를 Terraform으로 이관하고, 그 과정도 다뤄볼 예정이다.
'AI' 카테고리의 다른 글
| Swagger MCP Server 만들기 3편 - DB 저장 설계와 트러블 슈팅 (0) | 2026.02.17 |
|---|---|
| Swagger MCP Server 만들기 2편 - Swagger API 파싱 (2) | 2026.02.13 |
| Swagger MCP Server 만들기 1편 - LLM 기반 API 탐색 자동화 설계 (0) | 2026.02.05 |