Skip to main content
Summary

서버를 만들기 전에 경계부터 정한다

MCP 서버를 직접 만든다는 말은 "AI에게 우리 시스템의 어떤 문을 열어 줄 것인가"를 정한다는 뜻이다. 코드는 그다음 문제다.

좋은 MCP 서버는 기능이 많은 서버가 아니다. 책임 범위가 분명한 서버다. 파일도 읽고, DB도 조회하고, Slack도 보내고, 배포도 실행하는 서버는 처음에는 편해 보인다. 하지만 장애가 났을 때 원인을 찾기 어렵고, 권한을 줄이기도 어렵고, 사용자가 "이 AI가 정확히 무엇을 할 수 있는지" 이해하기도 어렵다.

따라서 첫 질문은 이것이다.

이 서버는 어떤 업무 경계를 대표하는가?

예를 들어 sales-report-mcp는 매출 리포트 조회와 초안 작성을 담당할 수 있다. github-review-mcp는 PR, 이슈, 릴리스 노트 작업을 담당할 수 있다. internal-approval-mcp는 승인 요청 조회와 승인 초안 생성을 담당할 수 있다. 이렇게 이름만 봐도 범위가 드러나야 한다.

MCP에서 Tool은 모델이 호출할 수 있는 실행 기능이고, name, description, inputSchema 같은 메타데이터로 노출된다. 클라이언트는 tools/list로 목록을 보고 tools/call로 실행한다.1 이 말은 곧, 서버가 제공하는 설명과 스키마가 모델의 행동을 좌우한다는 뜻이다. 설계가 흐리면 모델의 호출도 흐려진다.

직접 만드는 MCP 서버의 설계 원칙AI 생성 이미지

MCP화하기 좋은 후보 찾기

모든 기능을 MCP로 만들 필요는 없다. MCP화하기 좋은 후보는 대체로 네 가지다.

첫째, 사내 API다. 이미 REST API나 GraphQL API가 있고, 사람들이 매번 관리자 페이지를 열어 조회하는 정보라면 좋은 후보가 된다. 예를 들어 고객 계약 상태 조회, 배포 상태 조회, 장애 티켓 조회, 사내 제품 카탈로그 조회가 여기에 속한다.

둘째, 반복 스크립트다. 개발자나 운영자가 터미널에서 자주 실행하는 스크립트가 있다면 MCP Tool로 감쌀 수 있다. 예를 들어 "지난 24시간 에러 로그 요약", "릴리스 노트 초안 생성", "테스트 데이터 생성" 같은 작업이다. 다만 서버에 임의의 shell 명령 실행 권한을 열어 주면 안 된다. 기존 스크립트를 그대로 노출하기보다, 허용된 동작만 작은 Tool로 분리해야 한다.

셋째, 데이터 조회다. 데이터베이스 테이블, 로그, 문서 저장소, 분석 지표처럼 AI가 읽고 요약하면 가치가 생기는 정보가 있다. 이때는 처음부터 "SQL 실행 도구"를 만들기보다 "주문 상태 조회", "지난주 가입자 지표 조회", "특정 서비스의 에러율 조회"처럼 업무 단위로 좁히는 편이 안전하다.

넷째, 업무 승인 절차다. 승인 자체를 자동화하기보다, 승인에 필요한 정보를 모으고, 검토 문안을 만들고, 승인 요청 초안을 생성하는 기능부터 시작한다. 실제 승인 버튼은 나중에 붙여도 된다. 그리고 붙이더라도 사람 확인, 변경 내용 요약, 감사 로그가 있어야 한다.

후보를 고를 때는 다음 표처럼 정리해 보자.

후보MCP화 적합도처음 만들 형태
고객 정보 전체 검색낮음범위가 넓고 개인정보 위험이 큼
고객 ID로 계약 상태 조회높음Resource 또는 읽기 Tool
임의 SQL 실행낮음위험함. 제한된 조회 Tool로 대체
이번 주 장애 티켓 요약높음Resource + Prompt
Slack 전체 채널 발송낮음초안 생성 후 수동 발송부터 시작
승인 요청 초안 만들기높음Prompt 또는 Tool

핵심은 "AI가 편하게 하도록"이 아니라 "사람이 안심하고 맡길 수 있도록" 설계하는 것이다.

Tool, Resource, Prompt를 나누는 기준

MCP 서버는 기능을 Tool, Resource, Prompt로 나누어 제공할 수 있다. 이 구분은 단순한 형식 문제가 아니다. 권한, 승인, UI, 로그의 기준이 된다.

Resource는 읽을 수 있는 자료다. 문서, 로그, DB 레코드, 설정 정보, API 응답처럼 AI가 참고할 맥락을 URI로 제공한다. MCP Resource는 URI로 식별되고, 클라이언트는 resources/list와 resources/read로 목록을 보고 내용을 읽는다.2

예를 들어 다음은 Resource로 어울린다.

sales://reports/2026-07
incident://tickets/INC-1042
schema://warehouse/orders
policy://security/vendor-review

이런 항목은 "실행"보다 "참고"에 가깝다. AI가 읽고 요약하거나 비교하면 된다.

Tool은 실행 기능이다. 외부 API를 호출하거나 계산을 수행하거나 상태를 바꾸는 기능이다. "계약 상태 조회"처럼 읽기 작업도 Tool이 될 수 있지만, 입력을 받아 서버가 작업을 수행한다는 점에서 Resource와 다르다. 특히 생성, 수정, 삭제, 발송, 승인처럼 세상에 흔적을 남기는 작업은 Tool로 다뤄야 한다.

예를 들면 다음은 Tool에 가깝다.

get_contract_status(customerId)
create_jira_issue(projectKey, summary, description)
post_slack_draft(channelId, text)
approve_expense_request(requestId, decision)

Prompt는 반복 업무 양식이다. 사용자가 명시적으로 선택하는 워크플로 템플릿이다. MCP Prompt는 prompts/list로 발견되고 prompts/get으로 가져오며, 인자를 받아 구조화된 메시지를 반환할 수 있다.3

예를 들어 "장애 보고서 초안 작성", "PR 리뷰 체크리스트", "승인 요청 검토"는 Prompt로 만들기 좋다. Prompt는 직접 세상을 바꾸지 않는다. 대신 AI가 어떤 순서로 생각하고 어떤 형식으로 답해야 하는지 안내한다.

간단한 판단 기준은 이렇다.

  • 읽고 참고할 데이터인가? → Resource

  • 어떤 동작을 실행해야 하는가? → Tool

  • 반복되는 대화 흐름이나 문서 양식인가? → Prompt

실무에서는 셋을 함께 쓴다. 예를 들어 장애 보고 MCP 서버라면 incident://tickets/{id} Resource로 티켓을 읽고, get_recent_errors(serviceName, hours) Tool로 로그 요약을 가져오고, incident_report_prompt(ticketId) Prompt로 보고서 양식을 제공할 수 있다.

입력 스키마는 좁을수록 좋다

LLM은 사람처럼 문맥을 이해하지만, 항상 정확한 API 호출을 만드는 것은 아니다. 그래서 MCP Tool의 입력 스키마는 "친절한 자유 입력"보다 "좁고 명확한 선택지"가 좋다.

나쁜 예를 보자.

{
  "type": "object",
  "properties": {
    "query": { "type": "string" }
  }
}

이 스키마는 너무 넓다. query에 고객명, SQL, 자연어, 내부 명령, 엉뚱한 날짜가 모두 들어올 수 있다. 서버는 무엇을 허용해야 할지 매번 해석해야 한다.

좋은 스키마는 업무 의도를 분리한다.

{
  "type": "object",
  "properties": {
    "customerId": {
      "type": "string",
      "pattern": "^cus_[a-zA-Z0-9]{8,}$",
      "description": "조회할 고객 ID"
    },
    "includeInvoices": {
      "type": "boolean",
      "default": false,
      "description": "최근 청구서 요약을 함께 포함할지 여부"
    }
  },
  "required": ["customerId"],
  "additionalProperties": false
}

여기에는 몇 가지 원칙이 들어 있다.

첫째, 자유문 대신 식별자를 받는다. 고객명보다 고객 ID, 저장소 이름보다 저장소 ID, 채널명보다 채널 ID가 안전하다.

둘째, enum을 적극적으로 쓴다. 상태 변경 Tool이라면 decision을 문자열 전체로 받지 말고 "approve", "reject", "request_changes" 중 하나로 제한한다.

셋째, 범위를 숫자로 제한한다. limit은 최대 50, 날짜 범위는 최대 31일처럼 서버에서 검증한다.

넷째, 임의 코드와 임의 쿼리를 받지 않는다. sql, shellCommand, script, url 같은 필드는 매우 신중해야 한다. 꼭 필요하다면 허용 목록, 읽기 전용 계정, 네트워크 제한, 실행 시간 제한을 함께 둔다.

다섯째, 출력도 구조화한다. 가능하다면 outputSchema를 정의해 결과 형태를 일정하게 유지한다. 모델이 후속 작업에서 결과를 잘못 해석하는 일을 줄일 수 있다.

스키마는 문서가 아니라 안전장치다. 모델이 잘못 부르더라도 서버가 거절할 수 있어야 한다.

읽기부터 만들고 쓰기는 나중에 붙인다

직접 만드는 MCP 서버의 가장 안전한 확장 순서는 읽기 → 초안 → 승인된 쓰기다.

1단계는 읽기 전용 서버다. 조회, 검색, 요약에 필요한 기능만 제공한다. 예를 들어 티켓 조회, PR 목록 조회, 매출 지표 조회, 정책 문서 읽기다. 이 단계에서는 사용자가 "AI가 우리 데이터를 제대로 이해하는지" 확인한다.

2단계는 초안 생성이다. 이슈 생성이 아니라 이슈 초안 작성, Slack 발송이 아니라 메시지 초안 작성, 승인 실행이 아니라 승인 의견 초안 작성이다. 결과는 사람이 복사하거나 별도 UI에서 확인한다.

3단계는 제한된 쓰기다. 이때부터는 설계가 더 엄격해야 한다.

  • 실행 전 입력값을 사용자에게 보여 준다.

  • 변경 대상과 변경 내용을 요약한다.

  • dryRun 또는 미리보기 기능을 제공한다.

  • 승인 토큰이나 확인 플래그를 요구한다.

  • 누가, 언제, 어떤 입력으로, 어떤 결과를 만들었는지 로그를 남긴다.

  • 삭제·발송·승인 같은 작업은 별도 Tool로 분리한다.

Tool annotations도 도움이 된다. readOnlyHint, destructiveHint, idempotentHint, openWorldHint 같은 힌트는 클라이언트가 확인 창, 경고, 재시도 정책을 정하는 데 참고할 수 있다. 다만 이것은 보안 통제가 아니라 행동 설명에 가깝다. 실제 안전은 서버의 권한 검증, 입력 검증, 승인 절차, 네트워크 제한에서 나온다.4

즉, "이 Tool은 destructive가 아니라고 표시했으니 안전하다"가 아니다. "이 Tool은 어떤 위험이 있는지 클라이언트와 사용자에게 더 잘 설명한다"에 가깝다.

설명문은 사용자에게, 내부 이름은 개발자에게 맞춘다

MCP 서버를 만들 때 흔한 실수는 내부 함수 이름을 그대로 노출하는 것이다.

syncCrmV2LegacyStatus
runOpsScript
handleApprovalMutation

개발자에게는 익숙할 수 있다. 하지만 모델과 사용자에게는 모호하다. 더 나은 노출 이름은 다음과 같다.

get_customer_contract_status
summarize_recent_service_errors
create_expense_approval_draft

여기서 중요한 구분이 있다.

  • 내부 구현 이름: 코드 유지보수를 위한 이름

  • MCP 노출 이름: 모델이 호출할 기능 식별자

  • 사용자 표시 제목: 사람이 UI에서 이해할 이름

  • 설명문: 언제, 어떤 입력으로, 무엇을 하는지 알려 주는 문장

일부 프레임워크는 내부 메서드와 MCP에 노출되는 이름·설명을 별도로 지정할 수 있다. 예를 들어 Spring AI의 MCP annotation 방식에서도 Java 메서드명과 MCP Tool의 name, description은 분리될 수 있다.5 TypeScript나 Python으로 직접 구현하더라도 같은 원칙을 적용하면 된다.

설명문은 짧지만 구체적이어야 한다.

나쁜 설명:

고객 정보를 가져옵니다.

좋은 설명:

고객 ID로 계약 상태, 플랜, 갱신일을 조회합니다. 개인정보 원문과 결제 수단 정보는 반환하지 않습니다.

좋은 설명은 모델의 선택을 돕고, 사용자에게도 안심감을 준다. 특히 비슷한 Tool이 여러 개 있을 때 설명문 품질이 호출 정확도를 크게 좌우한다.

작은 서버가 오래 간다

처음 만드는 MCP 서버는 작아야 한다. 기능이 작다는 뜻이 아니라 책임이 작다는 뜻이다.

좋은 첫 서버의 조건은 다음과 같다.

  • 한 업무 영역만 다룬다.

  • 읽기 기능이 중심이다.

  • Tool 입력 스키마가 좁다.

  • Resource URI 구조가 예측 가능하다.

  • Prompt는 반복 업무 하나를 명확히 돕는다.

  • 쓰기 기능은 초안 또는 승인 후 실행으로 제한한다.

  • 사용자에게 보이는 이름과 설명이 내부 구현보다 명확하다.

  • 로그와 오류 메시지가 남는다.

MCP 서버 설계는 "AI에게 무엇을 시킬까"보다 "AI에게 무엇을 시키지 않을까"를 정하는 일에 가깝다. 좋은 경계가 있으면 코드는 단순해진다. 나쁜 경계 위에서는 아무리 코드를 잘 짜도 운영이 어려워진다.

다음 장에서는 이 원칙을 바탕으로 첫 MCP 서버를 실제로 만들어 볼 것이다. 하지만 기억해야 할 출발점은 하나다. MCP 서버의 품질은 첫 줄의 코드가 아니라, 첫 번째 기능 경계에서 결정된다.

참고

1Tools - Model Context Protocol

2Resources - Model Context Protocol

3Prompts - Model Context Protocol

4Tool Annotations as Risk Vocabulary: What Hints Can and Can't Do

5MCP Annotations :: Spring AI Reference

책 목록

MCP: AI 에이전트 연결의 표준

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기