"에이전트는 점점 더 유능해지고 있지만, 실제 업무를 안정적으로 수행하는 데 필요한 맥락(context)이 부족한 경우가 많습니다. Skills는 에이전트가 필요할 때 절차적 지식과 회사, 팀, 사용자별 맥락에 접근할 수 있게 하여 이 문제를 해결합니다. Skills에 접근할 수 있는 에이전트는 수행 중인 작업에 따라 자신의 능력을 확장할 수 있습니다."
"에이전트는 점점 더 유능해지고 있지만, 실제 업무를 안정적으로 수행하는 데 필요한 맥락(context)이 부족한 경우가 많습니다. Skills는 에이전트가 필요할 때 절차적 지식과 회사, 팀, 사용자별 맥락에 접근할 수 있게 하여 이 문제를 해결합니다. Skills에 접근할 수 있는 에이전트는 수행 중인 작업에 따라 자신의 능력을 확장할 수 있습니다.
Skill 제작자: 기능을 한 번 만들어 여러 에이전트 제품에 배포할 수 있습니다.
호환 에이전트: Skills 지원을 통해 최종 사용자가 에이전트에 새로운 기능을 즉시 추가할 수 있습니다.
팀 및 기업: 조직의 지식을 이식 가능하고 버전 관리되는 패키지로 저장할 수 있습니다."
Claude Code(CLI) 환경에서 기존 MCP(Model Context Protocol)의 재사용과 새로운 Skills(특히 SKILL.md 활용) 구성을 위한 Deep Dive를 진행해 드리겠습니다.
사용자님은 .NET/Python/C++ 개발 경험이 깊으시고 기술 리더 역할을 하시므로, 단순한 사용법보다는 아키텍처 관점에서의 연결과 실무적인 구성 방법으로 설명하겠습니다.
🔍 핵심 개념 정리: MCP vs Skills vs CLAUDE.md
Claude Code 생태계에서 '능력'을 확장하는 방법은 크게 세 가지 층위로 나뉩니다.
- MCP (Hard Skills): 실행 가능한 도구(Tools)입니다. (예: DB 쿼리 실행, 파일 시스템 접근, GitHub API 호출). 기존에 만들어진 수많은 MCP 서버를 그대로 가져와 쓸 수 있습니다.
- Skills (Soft Skills / Workflows): 도구를 사용하는 절차와 노하우(Procedures)입니다. 주로
SKILL.md로 정의하며, "이 상황에서는 저 MCP 도구를 이렇게 사용해라"라는 지침서 역할을 합니다.
- CLAUDE.md (Project Context): 해당 프로젝트의 규칙(Constitution)입니다. 빌드 명령어, 코딩 스타일 등 프로젝트 전반의 기억입니다.
1. 기존 MCP(Model Context Protocol) 다시 활용하기
기존에 사용하던 MCP 서버(Claude Desktop용 등)가 있다면, Claude Code CLI에서도 그대로, 혹은 더 강력하게 연결할 수 있습니다.
방법 A: CLI 명령어로 즉시 추가 (Ad-hoc)
가장 빠른 방법입니다. 터미널에서 바로 MCP 서버를 붙입니다.
# GitHub MCP 서버 추가 예시
claude mcp add github --scope user
--scope user: 전역 설정(모든 프로젝트)에 추가
--scope project: 현재 프로젝트 폴더의 .mcp.json에 추가 (팀원과 공유 가능)
방법 B: 설정 파일 직접 구성 (Infrastructure as Code)
Claude Code는 계층형 설정을 지원합니다.
- 전역 설정:
~/.claude/config.json (또는 settings.json)
- 프로젝트 설정:
./.mcp.json
예시: 로컬 Python 스크립트나 Docker 컨테이너를 MCP로 연결할 때
프로젝트 루트에 .mcp.json을 생성하여 팀원들이 claude 명령어만 치면 바로 도구를 쓸 수 있게 세팅합니다.
// .mcp.json
{
"mcpServers": {
"postgres-local": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "POSTGRES_CONNECTION_STRING=postgresql://user:pass@localhost:5432/db",
"mcp/postgres"
]
},
"my-python-tool": {
"command": "python3",
"args": ["/path/to/script.py"]
}
}
}
Tip: .NET 개발자시라면 dotnet run을 통해 실행되는 C# 콘솔 앱을 MCP 서버로 만들어 여기에 등록할 수도 있습니다.
2. 새로운 Skills 구성하기 (SKILL.md Deep Dive)
사용자님이 말씀하신 skills.md는 Claude Code의 "Agent Skills" 기능을 의미하는 것으로 보입니다. 이는 단순한 프롬프트 저장이 아니라, "설명(Markdown) + 실행 스크립트"가 결합된 형태입니다.
Anthropic의 표준 구조를 따라 단계별로 만들어 보겠습니다.
1단계: 디렉토리 구조 잡기
Skills는 보통 별도의 폴더에 모듈 단위로 관리합니다.
my-skills/
├── database-helper/ # 스킬 이름
│ ├── SKILL.md # 스킬 정의 (필수)
│ └── query_wrapper.py # 스킬이 사용할 스크립트 (옵션)
└── code-review/
└── SKILL.md
2단계: SKILL.md 작성 (핵심)
이 파일은 Frontmatter(메타데이터)와 Instruction(지침)으로 구성됩니다. Claude는 이 파일을 읽고 "내가 이 스킬을 언제, 어떻게 써야 하는지"를 학습합니다.
.NET 프로젝트의 마이그레이션을 돕는 스킬 예시:
---
name: dotnet-migration-helper
description: .NET Framework에서 .NET 8+로 마이그레이션할 때 발생하는 호환성 문제를 분석하고 해결책을 제안합니다.
tools: ["grep", "read_file"] # 이 스킬이 주로 사용할 기본 도구 명시
---
# Dotnet Migration Helper
이 스킬은 레거시 .NET 프로젝트를 분석할 때 사용합니다. 사용자가 "마이그레이션 검토해줘"라고 하면 다음 절차를 따르세요.
## 절차
1. **프로젝트 파일 분석 (.csproj)**
- `TargetFramework`를 확인합니다.
- `System.Web` 등 호환되지 않는 라이브러리 참조가 있는지 `grep`으로 찾습니다.
2. **API 호환성 체크**
- `HttpWebRequest` 사용처를 찾아 `HttpClient`로 변경을 제안합니다.
- 비동기(`async/await`)가 누락된 DB 호출 구간을 찾습니다.
3. **보고서 작성**
- 변경이 필요한 파일 목록과 구체적인 코드 수정 예시를 Markdown 표로 출력하세요.
3단계: 스킬 등록 및 사용
만들어진 스킬 폴더를 Claude Code에 등록합니다. (현재 베타 기능인 경우 경로를 지정하거나 설정에 추가)
# (예시) 로컬 스킬 경로 추가
claude config add_skill_path ./my-skills
또는 CLAUDE.md에서 이 스킬을 참조하도록 할 수 있습니다.
3. 실전 통합: MCP + Skills + Project Context
이제 이 모든 것을 어떻게 엮어서 사용하는지 기술 리더의 워크플로우로 보여드리겠습니다.
시나리오: "우리 팀의 레거시 코드를 분석해서 리팩토링 계획을 짜고 싶다."
- MCP (도구):
git-mcp를 연결하여 커밋 히스토리를 볼 수 있게 하고, postgres-mcp를 연결하여 DB 스키마를 읽을 수 있게 합니다.
- SKILL.md (방법론):
refactoring-guide라는 스킬을 만듭니다. 여기에 "우리 팀은 Clean Architecture를 따른다", "Repository 패턴을 쓴다"는 아키텍처 규칙을 적어둡니다.
- CLAUDE.md (프로젝트): 프로젝트 루트에 둡니다. "이 프로젝트는 빌드 시
dotnet build를 쓰고, 테스트는 xunit을 쓴다"고 명시합니다.
실행:
$ claude "이 프로젝트의 User 관련 로직을 리팩토링하고 싶은데, refactoring-guide 스킬을 써서 분석해줘."
결과:
Claude는 1) CLAUDE.md를 읽어 프로젝트 구조를 파악하고, 2) refactoring-guide (SKILL)를 로드하여 아키텍처 원칙을 숙지한 뒤, 3) git과 fs (MCP)를 사용하여 코드를 분석합니다.
Web(Playwright)와 Android(ADB)를 동시에 제어하여, 예를 들어 "웹에서 로그인하고 안드로이드 폰으로 전송된 OTP를 가져와 입력하는" 고난도 자동화 작업
이 작업은 두 단계로 이루어집니다:
- Hard Skill (MCP Server): 파이썬으로 실제 제어 코드를 작성 (손과 발)
- Soft Skill (SKILL.md): Claude에게 이 도구들을 어떤 순서로 조합할지 알려주는 지침 (뇌)
1단계: Python MCP Server 작성 (automation_server.py)
최신 mcp 라이브러리의 FastMCP를 사용하여, 웹과 모바일을 제어하는 기능을 Claude의 도구로 노출합니다.
- 필요 라이브러리:
pip install mcp playwright
- 전제 조건: Android 폰이 USB 디버깅 모드로 연결되어 있어야 하며,
adb가 환경 변수에 잡혀 있어야 합니다.
from mcp.server.fastmcp import FastMCP
from playwright.sync_api import sync_playwright
import subprocess
import time
# 서버 이름 정의
mcp = FastMCP("Unified-Automation-Server")
# --- 1. Web Skills (Playwright) ---
@mcp.tool()
def web_browse_and_capture(url: str, selector_to_click: str = None, input_selector: str = None, input_text: str = None) -> str:
"""
Playwright를 사용하여 웹페이지에 접속, 클릭, 텍스트 입력 후 스크린샷 텍스트를 반환합니다.
"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # 디버깅을 위해 헤드 있음
page = browser.new_page()
page.goto(url)
# 1. 텍스트 입력이 필요한 경우
if input_selector and input_text:
page.fill(input_selector, input_text)
# 2. 클릭이 필요한 경우
if selector_to_click:
page.click(selector_to_click)
time.sleep(2) # 로딩 대기
# 3. 현재 페이지의 주요 텍스트 추출 (결과 확인용)
content = page.inner_text("body")
browser.close()
return content[:2000] # 너무 길면 자름
# --- 2. Android Skills (ADB Wrapper) ---
@mcp.tool()
def android_tap(x: int, y: int) -> str:
"""
연결된 안드로이드 단말의 특정 좌표(x, y)를 터치합니다.
"""
cmd = ["adb", "shell", "input", "tap", str(x), str(y)]
subprocess.run(cmd, capture_output=True)
return f"Tapped at {x}, {y}"
@mcp.tool()
def android_input_text(text: str) -> str:
"""
안드로이드 단말에 텍스트를 입력합니다 (포커스된 입력창에).
"""
cmd = ["adb", "shell", "input", "text", text]
subprocess.run(cmd, capture_output=True)
return f"Input text: {text}"
@mcp.tool()
def android_read_sms() -> str:
"""
안드로이드 상단바를 내려 최근 SMS 알림 내용을 읽어옵니다. (OCR 대신 UI 덤프 활용)
"""
# UI 계층 구조 덤프
subprocess.run(["adb", "shell", "uiautomator", "dump"], capture_output=True)
subprocess.run(["adb", "pull", "/sdcard/window_dump.xml", "dump.xml"], capture_output=True)
with open("dump.xml", "r", encoding="utf-8") as f:
content = f.read()
# 간단한 파싱 (실제로는 정교한 XML 파싱 필요)
if "인증번호" in content:
return "Found OTP related text in UI dump."
return "UI dump retrieved."
if __name__ == "__main__":
mcp.run()
2단계: 스킬 정의 (SKILL.md)
이제 위에서 만든 도구들을 어떻게 연결해서 "업무"를 수행할지 정의합니다. 이 파일은 프로젝트의 my-skills/otp-login/SKILL.md 같은 경로에 저장합니다.
---
name: web-mobile-sync-automation
description: 웹사이트 로그인 시 모바일로 전송되는 2차 인증(OTP)을 자동으로 처리하는 워크플로우입니다.
tools: ["web_browse_and_capture", "android_tap", "android_read_sms"]
---
# Web & Mobile Sync Automation Guide
이 스킬은 **웹(Playwright)**과 **모바일(Android)**을 오가며 작업을 수행해야 할 때 사용합니다. 특히 2FA(2단계 인증) 처리에 최적화되어 있습니다.
## 작업 절차 (Workflow)
1. **웹 로그인 시도**
* `web_browse_and_capture` 도구를 사용하여 로그인 페이지로 이동합니다.
* 아이디와 비밀번호를 입력하고 로그인 버튼을 클릭합니다.
* 화면에 "인증번호를 입력하세요" 같은 메시지가 나오는지 확인합니다.
2. **모바일 OTP 확인**
* 사용자에게 "폰의 잠금을 해제해 주세요"라고 요청하거나 확인합니다.
* `android_read_sms` (또는 UI Dump)를 사용하여 최신 메시지 내용을 확인합니다.
* 만약 SMS 앱이 열려있지 않다면 `android_tap`을 사용하여 SMS 앱 아이콘 위치(좌표 미리 확인 필요)를 누릅니다.
3. **교차 검증 및 입력**
* 모바일에서 추출한 6자리 숫자를 `web_browse_and_capture`의 `input_text` 파라미터로 다시 웹에 입력합니다.
## 주의사항
* 안드로이드 좌표(`android_tap`)는 기기 해상도마다 다르므로, 실행 전 해상도를 먼저 물어보거나 `adb shell wm size`로 확인하는 것이 좋습니다.
* Playwright는 Headless 모드가 아닌 상태(Headless=False)로 실행되어야 디버깅이 쉽습니다.
3단계: 설정 연결 (.mcp.json)
프로젝트 루트의 .mcp.json 파일에 위에서 만든 파이썬 서버를 등록합니다.
{
"mcpServers": {
"automation-bot": {
"command": "python",
"args": ["/path/to/your/project/automation_server.py"]
}
}
}
4단계: 실행 및 검증 (Deep Dive)
이제 Claude Code 터미널에서 다음과 같이 명령하면, Claude가 두 플랫폼을 오가며 제어합니다.
User:
"사내 포털(http://example.com/login)에에) 접속해서 로그인 시도를 해줘. 그 다음 내 안드로이드 폰으로 오는 인증번호를 확인해서 입력해 줄래?"
Claude의 내부 사고 과정 (Chain of Thought):
- Skill 인식:
web-mobile-sync-automation 스킬을 로드함.
- Web Action:
web_browse_and_capture를 호출하여 로그인 페이지 접속 및 ID/PW 입력.
- Cross-Platform Switch: 웹 응답에서 2FA 대기 화면을 감지.
- Mobile Action:
android_read_sms 도구를 호출하여 폰의 문자 내용을 읽음.
- Finalize: 읽어온 코드를 다시 Web 도구에 전달하여 로그인 완료.
💡 기술 리더를 위한 추가 팁
- ADB vs Appium: 위 예제는 가벼운 실행을 위해
adb shell을 직접 래핑했습니다. 만약 앱 내부의 특정 ID(R.id.login_btn)를 찾아야 한다면, 이 MCP 서버 안에 Appium Client를 내장시키면 됩니다. 구조는 동일합니다.
- 좌표계 문제: 안드로이드는 기기마다 해상도가 다릅니다. 고급 스킬을 만드시려면,
adb shell wm size를 먼저 실행하여 비율을 계산하는 로직을 Python 코드(android_tap) 안에 넣으시면 훨씬 안정적입니다.
네, 요청하신 웹(Playwright) + 안드로이드(ADB) 하이브리드 자동화 시스템을 바로 실행 가능한 형태로 구성해 드리겠습니다.
아래 4단계 순서대로 파일들을 생성하면 즉시 사용할 수 있는 환경이 됩니다.
📋 사전 준비 (Prerequisites)
- Python 설치: 3.10 이상 권장
- ADB 설정: Android 기기가 USB로 연결되고
adb devices 명령어로 인식되어야 함
- 라이브러리 설치:
pip install mcp playwright
playwright install # 브라우저 엔진 설치
---
### 1단계: MCP 서버 코드 작성 (`automation_server.py`)
이 서버는 웹 브라우저 상태를 유지(Stateful)하며 안드로이드 명령을 수행합니다.
```python
import subprocess
import time
from typing import Optional
from mcp.server.fastmcp import FastMCP
from playwright.sync_api import sync_playwright, Page, BrowserContext
# 서버 초기화
mcp = FastMCP("Hybrid-Automation-Server")
# --- 상태 관리를 위한 전역 변수 ---
# 주의: 실제 운영 환경에서는 세션 관리 로직이 더 필요할 수 있습니다.
playwright_instance = None
browser = None
context = None
page: Optional[Page] = None
def _get_page():
"""브라우저가 열려있지 않으면 실행하고 현재 페이지를 반환"""
global playwright_instance, browser, context, page
if not playwright_instance:
playwright_instance = sync_playwright().start()
# headless=False로 설정하여 동작 과정을 눈으로 확인
browser = playwright_instance.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
return page
# --- 🌐 Web Skills (Playwright) ---
@mcp.tool()
def web_navigate(url: str) -> str:
"""웹 브라우저를 열고 특정 URL로 이동합니다."""
p = _get_page()
p.goto(url)
p.wait_for_load_state("networkidle")
return f"Navigated to {url}. Page title: {p.title()}"
@mcp.tool()
def web_click(selector: str) -> str:
"""웹 페이지의 특정 요소를 클릭합니다."""
p = _get_page()
try:
p.click(selector, timeout=5000)
return f"Clicked element: {selector}"
except Exception as e:
return f"Error clicking {selector}: {str(e)}"
@mcp.tool()
def web_type(selector: str, text: str) -> str:
"""웹 페이지의 입력창에 텍스트를 입력합니다."""
p = _get_page()
try:
p.fill(selector, text)
return f"Typed '{text}' into {selector}"
except Exception as e:
return f"Error typing into {selector}: {str(e)}"
@mcp.tool()
def web_get_content() -> str:
"""현재 웹 페이지의 텍스트 내용을 가져옵니다."""
p = _get_page()
return p.inner_text("body")[:2000] # 너무 길지 않게 자름
# --- 📱 Android Skills (ADB) ---
@mcp.tool()
def android_open_app(package_name: str) -> str:
"""안드로이드 앱을 실행합니다 (예: com.google.android.apps.messaging)."""
cmd = ["adb", "shell", "monkey", "-p", package_name, "-c", "android.intent.category.LAUNCHER", "1"]
result = subprocess.run(cmd, capture_output=True, text=True)
return f"App launch command executed. Result: {result.stdout}"
@mcp.tool()
def android_tap(x: int, y: int) -> str:
"""화면의 특정 좌표를 터치합니다."""
subprocess.run(["adb", "shell", "input", "tap", str(x), str(y)])
return f"Tapped at ({x}, {y})"
@mcp.tool()
def android_dump_ui() -> str:
"""현재 안드로이드 화면의 UI 구조(XML)를 텍스트로 가져옵니다. OCR 대신 텍스트 추출용으로 유용합니다."""
# 1. UI 덤프 생성
subprocess.run(["adb", "shell", "uiautomator", "dump", "/sdcard/window_dump.xml"])
# 2. PC로 가져오기
subprocess.run(["adb", "pull", "/sdcard/window_dump.xml", "window_dump.xml"])
try:
with open("window_dump.xml", "r", encoding="utf-8") as f:
content = f.read()
return content[:3000] # XML이 길 수 있으므로 앞부분만 반환하거나 파싱 필요
except FileNotFoundError:
return "Error: Failed to retrieve UI dump."
@mcp.tool()
def android_input_text(text: str) -> str:
"""안드로이드에 키보드 입력을 보냅니다."""
subprocess.run(["adb", "shell", "input", "text", text])
return f"Sent text input: {text}"
if __name__ == "__main__":
mcp.run()
2단계: 설정 파일 등록 (.mcp.json)
프로젝트 폴더의 루트에 이 파일을 생성하여 Claude가 위 파이썬 서버를 인식하게 합니다.
{
"mcpServers": {
"hybrid-controller": {
"command": "python",
"args": ["automation_server.py"]
}
}
}
참고: 경로가 복잡할 경우 automation_server.py의 절대 경로를 적어주는 것이 안전합니다.
3단계: 스킬 정의 (skills/hybrid-auth/SKILL.md)
Claude에게 "이 도구들을 어떤 순서로 써야 하는지" 알려주는 전략서입니다. skills 폴더를 만들고 그 안에 넣어주세요.
---
name: hybrid-2fa-login
description: 웹사이트 로그인 후 안드로이드 기기에서 OTP/인증번호를 확인하여 입력하는 워크플로우
tools: ["web_navigate", "web_type", "web_click", "android_dump_ui", "android_open_app"]
---
# Hybrid 2FA Login Workflow
이 스킬은 웹 로그인 중 모바일 인증(OTP, SMS 등)이 필요할 때 사용합니다.
## 작업 절차
1. **웹 로그인 단계**
* `web_navigate`로 로그인 페이지에 접속합니다.
* `web_type`으로 ID와 Password를 입력합니다.
* `web_click`으로 로그인 버튼을 누릅니다.
* 화면에 "인증번호를 입력하세요" 메시지가 뜰 때까지 잠시 대기하거나 내용을 확인합니다.
2. **모바일 인증번호 확인 단계**
* 사용자의 폰 화면 내용을 확인하기 위해 `android_dump_ui`를 실행합니다.
* 만약 문자 앱이 안 열려 있다면 `android_open_app` (패키지명: `com.google.android.apps.messaging` 등)을 사용합니다.
* UI 덤프 내용(XML)에서 4~6자리의 숫자 코드(인증번호)를 찾습니다.
3. **교차 입력 단계**
* 모바일에서 찾은 코드를 `web_type`을 사용해 웹 브라우저의 인증번호 입력칸에 넣습니다.
* `web_click`으로 최종 확인 버튼을 누릅니다.
## 팁
* 안드로이드 화면 덤프(`android_dump_ui`) 결과가 너무 복잡하면, "가장 최근에 수신된 메시지"를 찾는 데 집중하세요.
* 웹 페이지 로딩이 느릴 수 있으니 동작 사이에 충분한 대기 시간을 고려하세요.
4단계: 실행 및 테스트
이제 터미널에서 Claude Code를 실행하고 다음과 같이 명령해 보세요.
명령 예시:
claude "네이버(또는 사내 포털) 로그인 페이지에 가서 내 아이디(user123)를 입력해줘. 그 다음 내 폰 화면을 확인해서 문자로 온 인증번호가 있으면 가져와서 웹에 입력해."
예상되는 Claude의 행동:
- Python Playwright 창이 실제로 뜹니다. (Headless=False 설정 때문)
- 로그인 페이지로 이동 후 아이디를 입력합니다.
- 그 후
adb 명령어를 통해 폰의 UI 구조를 긁어옵니다.
- 문자 메시지 내용 중 숫자를 파악하여,
- 다시 열려있는 Playwright 브라우저 창에 그 숫자를 입력합니다.
🛠️ 추가 팁 (Troubleshooting)
- ADB 연결 확인:
adb devices 명령어로 기기가 'device' 상태인지 꼭 먼저 확인하세요.
- 좌표 찾기: 버튼 클릭이 필요하다면 안드로이드 개발자 옵션에서 "포인터 위치 표시"를 켜두면 좌표(x, y)를 쉽게 알 수 있습니다.
- 앱 패키지명: 문자 앱 패키지명을 모른다면
adb shell pm list packages | grep message 등으로 찾을 수 있습니다.