chore: initial project setup with MCP server configs
- Add MCP servers for Redmine, Jenkins, Gitea, Obsidian - Add setup_mcp.sh to generate .mcp.json from .env (no secrets in VCS) - Add .env.example with required variable names for team onboarding - Add .gitignore to exclude secrets (.env, .mcp.json, settings.local.json) - Fix obsidian_mcp_server.py stdio_server async context manager usage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
25
.env.example
Normal file
25
.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# =============================================================
|
||||||
|
# 팀원별 환경 변수 설정 파일
|
||||||
|
# 사용법:
|
||||||
|
# cp .env.example .env
|
||||||
|
# .env에 본인 값 입력 후 저장
|
||||||
|
# ./setup_mcp.sh 실행 → Claude Code 재시작
|
||||||
|
#
|
||||||
|
# .env는 절대 Git에 커밋하지 마세요!
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Redmine
|
||||||
|
REDMINE_URL=https://redmine.cloud-handson.com
|
||||||
|
REDMINE_API_KEY=your_redmine_api_key
|
||||||
|
|
||||||
|
# Jenkins
|
||||||
|
JENKINS_URL=https://jenkins.cloud-handson.com
|
||||||
|
JENKINS_USER=your_jenkins_username
|
||||||
|
JENKINS_TOKEN=your_jenkins_api_token
|
||||||
|
|
||||||
|
# Gitea (URL은 /api/v1 포함)
|
||||||
|
GITEA_API_URL=https://gittea.cloud-handson.com/api/v1
|
||||||
|
GITEA_TOKEN=your_gitea_access_token
|
||||||
|
|
||||||
|
# Obsidian Vault (각자 로컬 경로에 맞게 수정)
|
||||||
|
OBSIDIAN_VAULT=/Users/your_username/Documents/Obsidian Vault
|
||||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 시크릿 / 환경 변수
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# MCP 설정 (시크릿 포함 → setup_mcp.sh로 생성)
|
||||||
|
.mcp.json
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# 구버전 MCP 등록 스크립트 (시크릿 하드코딩 → setup_mcp.sh로 대체)
|
||||||
|
add_jenkins_mcp.sh
|
||||||
|
add_redmine_mcp.sh
|
||||||
|
add_gittea_mcp.sh
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 에디터
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
207
CLAUDE.md
Normal file
207
CLAUDE.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# CLAUDE.md – 프로젝트 가이드
|
||||||
|
|
||||||
|
> 이 파일은 AI 코딩 어시스턴트(Claude, Cursor 등)가 이 저장소에서 작업할 때
|
||||||
|
> 따라야 할 규칙과 컨텍스트를 정의합니다. 반드시 숙지 후 작업하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프로젝트 개요
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 프로젝트명 | (프로젝트명을 여기에 입력하세요) |
|
||||||
|
| 목적 | (프로젝트의 목적을 한 줄로 설명하세요) |
|
||||||
|
| 기술 스택 | Python 3.11 · FastAPI · PostgreSQL 15 · React 18 · TypeScript |
|
||||||
|
| 아키텍처 | 모놀리스 (추후 마이크로서비스 전환 예정) |
|
||||||
|
| 주요 언어 | 한국어 (코드 내 주석 및 커밋 메시지는 영어 사용) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 디렉터리 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── backend/
|
||||||
|
│ ├── api/ # FastAPI 라우터 (엔드포인트 정의)
|
||||||
|
│ ├── models/ # SQLAlchemy DB 모델
|
||||||
|
│ ├── schemas/ # Pydantic 요청/응답 스키마
|
||||||
|
│ ├── services/ # 비즈니스 로직 레이어
|
||||||
|
│ └── core/ # 설정, 의존성 주입, 미들웨어
|
||||||
|
├── frontend/
|
||||||
|
│ └── src/
|
||||||
|
│ ├── components/ # 재사용 가능한 UI 컴포넌트
|
||||||
|
│ ├── pages/ # 라우트 단위 페이지
|
||||||
|
│ ├── hooks/ # 커스텀 React Hooks
|
||||||
|
│ └── api/ # API 클라이언트 (axios/fetch 래퍼)
|
||||||
|
├── tests/
|
||||||
|
│ ├── unit/
|
||||||
|
│ ├── integration/
|
||||||
|
│ └── e2e/
|
||||||
|
├── docs/ # 설계 문서
|
||||||
|
├── .env.example # 환경 변수 템플릿 (실제 값 절대 포함 금지)
|
||||||
|
└── Makefile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 코딩 컨벤션
|
||||||
|
|
||||||
|
### Python (Backend)
|
||||||
|
- PEP 8 준수, **type hints 필수** (매개변수 및 반환값 모두)
|
||||||
|
- Docstring: Google 스타일
|
||||||
|
- 함수는 단일 책임 원칙(SRP) 준수, 50줄 초과 지양
|
||||||
|
- 예외 처리 시 bare `except:` 금지, 반드시 구체적인 예외 타입 명시
|
||||||
|
|
||||||
|
### JavaScript / TypeScript (Frontend)
|
||||||
|
- ESLint + Prettier 설정 준수 (`npm run lint` 통과 필수)
|
||||||
|
- 컴포넌트는 **함수형**만 사용, 클래스 컴포넌트 금지
|
||||||
|
- `any` 타입 사용 금지, 불가피한 경우 `unknown` 후 타입 가드 사용
|
||||||
|
- 상태 관리: 로컬 상태는 `useState`, 전역 상태는 별도 문서 참조
|
||||||
|
|
||||||
|
### Git 브랜치 전략 (Gitflow)
|
||||||
|
```
|
||||||
|
main ← 프로덕션 배포 전용 (직접 푸시 절대 금지)
|
||||||
|
develop ← 통합 개발 브랜치
|
||||||
|
feature/* ← 기능 개발 (예: feature/user-auth)
|
||||||
|
hotfix/* ← 프로덕션 긴급 수정
|
||||||
|
release/* ← 릴리즈 준비
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커밋 메시지 (Conventional Commits)
|
||||||
|
```
|
||||||
|
feat: 새로운 기능 추가
|
||||||
|
fix: 버그 수정
|
||||||
|
docs: 문서 수정
|
||||||
|
test: 테스트 코드 추가/수정
|
||||||
|
refactor: 리팩터링 (기능 변경 없음)
|
||||||
|
chore: 빌드, 설정 파일 수정
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 환경 변수 및 시크릿 관리
|
||||||
|
|
||||||
|
- 모든 시크릿은 `.env` 파일에서 관리 (`.gitignore`에 반드시 포함)
|
||||||
|
- `.env.example`에 키 이름만 기재, 실제 값은 절대 커밋 금지
|
||||||
|
- AI가 코드를 작성할 때 API Key, 비밀번호, 토큰을 **하드코딩하지 말 것**
|
||||||
|
- 환경 변수 접근: `os.getenv("KEY_NAME")` 또는 `pydantic BaseSettings` 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 외부 시스템 연동
|
||||||
|
|
||||||
|
| 시스템 | URL | 용도 |
|
||||||
|
|--------|-----|------|
|
||||||
|
| 이슈 트래커 | https://redmine.cloud-handson.com | 작업 이슈 관리 |
|
||||||
|
| CI/CD | https://jenkins.cloud-handson.com | 자동 빌드/배포 |
|
||||||
|
| Git 서버 | https://gittea.cloud-handson.com | 소스 코드 관리 |
|
||||||
|
| 지식베이스 | Obsidian Vault (/Users/joungmin/Documents/Obsidian Vault) | 설계 문서 동기화, para document 관리법 사용 |
|
||||||
|
|
||||||
|
## MCP 툴
|
||||||
|
|
||||||
|
외부 시스템은 MCP(Model Context Protocol) 툴을 통해 직접 제어할 수 있습니다.
|
||||||
|
API 호출 대신 MCP 툴을 우선 사용하세요.
|
||||||
|
|
||||||
|
### Jenkins (`mcp__jenkins__*`)
|
||||||
|
|
||||||
|
| 툴 | 설명 |
|
||||||
|
|----|------|
|
||||||
|
| `get_jobs` | 잡 목록 조회 |
|
||||||
|
| `get_job_config` | 잡 config.xml 조회 |
|
||||||
|
| `create_job` | 새 잡 생성 (config.xml 필요) |
|
||||||
|
| `edit_job` | 잡 설정 수정 (config.xml 필요) |
|
||||||
|
| `delete_job` | 잡 삭제 |
|
||||||
|
| `trigger_build` | 빌드 트리거 |
|
||||||
|
| `get_build_status` | 빌드 상태 조회 |
|
||||||
|
| `get_build_log` | 빌드 로그 조회 |
|
||||||
|
|
||||||
|
### Redmine (`mcp__redmine__*`)
|
||||||
|
|
||||||
|
| 툴 | 설명 |
|
||||||
|
|----|------|
|
||||||
|
| `getIssues` / `getIssue` | 이슈 목록/상세 조회 |
|
||||||
|
| `createIssue` | 이슈 생성 |
|
||||||
|
| `updateIssue` | 이슈 수정 |
|
||||||
|
| `deleteIssue` | 이슈 삭제 |
|
||||||
|
| `getProjects` / `getProject` | 프로젝트 조회 |
|
||||||
|
| `createProject` / `updateProject` | 프로젝트 생성/수정 |
|
||||||
|
| `getTimeEntries` / `createTimeEntry` | 시간 기록 조회/생성 |
|
||||||
|
| `getWikiPage` / `updateWikiPage` | 위키 페이지 조회/수정 |
|
||||||
|
| `search` | 전체 검색 |
|
||||||
|
| `getCurrentUser` | 현재 로그인 사용자 조회 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개발 방법론
|
||||||
|
|
||||||
|
- 스프린트: **2주 주기 스크럼**
|
||||||
|
- 워크플로: `Redmine 이슈 생성` → `브랜치 생성` → `PR 작성` → `코드 리뷰` → `Jenkins CI 통과` → `develop 머지`
|
||||||
|
- 테스트 커버리지 목표: **unit 80%+**, integration 60%+
|
||||||
|
- PR 머지 전 최소 1명의 코드 리뷰 승인 필요
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 금기 사항 (AI 포함 모든 기여자 준수)
|
||||||
|
|
||||||
|
- `main` 브랜치 직접 푸시 **절대 금지**
|
||||||
|
- API Key, 비밀번호 등 시크릿 하드코딩 **절대 금지**
|
||||||
|
- `TODO` 주석은 반드시 Redmine 이슈 번호와 함께 작성 (`# TODO(#123): 설명`)
|
||||||
|
- 테스트 없이 핵심 비즈니스 로직 변경 금지
|
||||||
|
- `print()` 디버그 코드를 프로덕션 코드에 남기지 말 것 (logger 사용)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 개발 서버 시작 (backend + frontend 동시 실행)
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# 테스트 실행
|
||||||
|
make test
|
||||||
|
|
||||||
|
# 특정 테스트만 실행
|
||||||
|
pytest tests/unit/ -v
|
||||||
|
|
||||||
|
# 린트 검사
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# 린트 자동 수정
|
||||||
|
make lint-fix
|
||||||
|
|
||||||
|
# DB 마이그레이션 생성
|
||||||
|
alembic revision --autogenerate -m "description"
|
||||||
|
|
||||||
|
# DB 마이그레이션 적용
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참조 문서
|
||||||
|
|
||||||
|
| 문서 | 경로 | 내용 |
|
||||||
|
|------|------|------|
|
||||||
|
| 디자인 패턴 가이드 | [`SOFTWARE_GUIDE.md`](./SOFTWARE_GUIDE.md) | 생성·구조·행위 패턴 정의, 빠른 선택 가이드 포함 |
|
||||||
|
|
||||||
|
> 새 모듈·클래스 설계 시 `SOFTWARE_GUIDE.md`의 **빠른 선택 가이드**를 먼저 확인하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 작업 지침
|
||||||
|
|
||||||
|
> AI가 이 저장소에서 코드를 생성하거나 수정할 때 반드시 따라야 할 규칙입니다.
|
||||||
|
|
||||||
|
1. **기존 패턴 우선**: 새 코드 작성 전 인접 파일의 구조와 패턴을 파악하고 일관성 유지
|
||||||
|
2. **최소 변경 원칙**: 요청된 변경 범위를 벗어나는 수정은 하지 말 것
|
||||||
|
3. **테스트 동반 작성**: 새 함수/로직 작성 시 대응하는 단위 테스트 함께 제안
|
||||||
|
4. **불확실 시 질문**: 요구사항이 모호하면 임의로 결정하지 말고 반드시 확인 요청
|
||||||
|
5. **보안 우선**: 인증/인가 로직은 임의로 우회하거나 단순화하지 말 것
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 팀 연락처
|
||||||
|
|
||||||
|
| 역할 | 담당자 |
|
||||||
|
|------|--------|
|
||||||
|
| PM / Tech Lead | JM |
|
||||||
144
SOFTWARE_GUIDE.md
Normal file
144
SOFTWARE_GUIDE.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# 소프트웨어 디자인 패턴 참조 가이드
|
||||||
|
|
||||||
|
디자인 패턴은 반복적인 문제를 해결하는 검증된 방법론입니다.
|
||||||
|
코드 작성 전, 아래 **빠른 선택 가이드**로 패턴을 먼저 고르세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 선택 가이드
|
||||||
|
|
||||||
|
| 상황 | 적합한 패턴 | 분류 |
|
||||||
|
|------|------------|------|
|
||||||
|
| 인스턴스를 하나만 유지해야 할 때 | 싱글톤 | 생성 |
|
||||||
|
| 객체 생성이 복잡하거나 조건부일 때 | 팩토리 | 생성 |
|
||||||
|
| 매개변수가 많거나 선택적인 객체 생성 | 빌더 | 생성 |
|
||||||
|
| 기존 객체를 복제해서 확장할 때 | 프로토타입 | 생성 |
|
||||||
|
| 복잡한 API를 단순하게 감싸야 할 때 | 퍼사드 | 구조 |
|
||||||
|
| 호환되지 않는 인터페이스를 연결할 때 | 어댑터 | 구조 |
|
||||||
|
| 접근 제어·캐싱·지연 로딩이 필요할 때 | 프록시 | 구조 |
|
||||||
|
| 상태 변화를 여러 곳에 알려야 할 때 | 옵저버 | 행위 |
|
||||||
|
| 컬렉션을 표준 방식으로 순회할 때 | 이터레이터 | 행위 |
|
||||||
|
| 알고리즘을 런타임에 교체해야 할 때 | 전략 | 행위 |
|
||||||
|
| 복잡한 조건문(if/switch)을 제거할 때 | 스테이트 | 행위 |
|
||||||
|
| 객체 간 직접 통신을 줄이고 싶을 때 | 메디에이터 | 행위 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 생성 패턴 (Creational Patterns)
|
||||||
|
|
||||||
|
객체를 **어떻게 만들지**를 다루는 패턴
|
||||||
|
|
||||||
|
### 싱글톤 (Singleton)
|
||||||
|
- **목적**: 클래스 인스턴스를 전역에서 단 하나만 보장
|
||||||
|
- **사용 예**: 앱 설정 객체, 전역 로거, 캐시 관리자
|
||||||
|
- **특징**: 어디서 접근해도 동일한 인스턴스 반환
|
||||||
|
|
||||||
|
### 팩토리 (Factory)
|
||||||
|
- **목적**: 객체 생성 로직을 캡슐화, 클라이언트에서 `new`를 직접 사용하지 않음
|
||||||
|
- **사용 예**: 플랫폼별 UI 컴포넌트 생성, 버거 주문 시스템
|
||||||
|
- **특징**: 생성할 구체 클래스를 호출자로부터 분리
|
||||||
|
|
||||||
|
### 빌더 (Builder)
|
||||||
|
- **목적**: 복잡한 객체를 단계별로 조립
|
||||||
|
- **사용 예**: SQL 쿼리 빌더, HTTP 요청 객체, 설정이 많은 DTO
|
||||||
|
- **특징**: 메서드 체이닝 → `build()`로 최종 객체 완성
|
||||||
|
|
||||||
|
### 프로토타입 (Prototype)
|
||||||
|
- **목적**: 기존 객체를 복제(clone)하여 새 객체 생성
|
||||||
|
- **사용 예**: JavaScript 프로토타입 상속, 깊은 복사가 필요한 객체
|
||||||
|
- **특징**: 클래스 상속 없이 기능 확장 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 구조 패턴 (Structural Patterns)
|
||||||
|
|
||||||
|
객체와 클래스를 **어떻게 조합할지**를 다루는 패턴
|
||||||
|
|
||||||
|
### 퍼사드 (Facade)
|
||||||
|
- **목적**: 복잡한 내부 시스템을 단순한 인터페이스로 노출
|
||||||
|
- **사용 예**: 외부 API 래퍼, 복잡한 라이브러리의 진입점
|
||||||
|
- **특징**: 내부 구현 세부사항을 숨기고 고수준 API 제공
|
||||||
|
|
||||||
|
### 어댑터 (Adapter)
|
||||||
|
- **목적**: 호환되지 않는 두 인터페이스를 연결
|
||||||
|
- **사용 예**: 레거시 코드 통합, 서드파티 라이브러리 래핑
|
||||||
|
- **특징**: 기존 클래스를 수정하지 않고 새 인터페이스에 맞춤
|
||||||
|
|
||||||
|
### 프록시 (Proxy)
|
||||||
|
- **목적**: 실제 객체 앞에 대리 객체를 두어 접근 제어·부가 기능 추가
|
||||||
|
- **사용 예**: Vue.js 반응형 시스템, 지연 로딩(Lazy Loading), API 캐싱
|
||||||
|
- **특징**: 실제 객체 교체 없이 동작 가로채기 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 행위 패턴 (Behavioral Patterns)
|
||||||
|
|
||||||
|
객체 간 **통신과 책임 분배**를 다루는 패턴
|
||||||
|
|
||||||
|
### 옵저버 (Observer / Pub-Sub)
|
||||||
|
- **목적**: 한 객체의 상태 변화를 구독자들에게 자동으로 알림
|
||||||
|
- **사용 예**: Firebase 실시간 업데이트, 유튜브 구독 알림, 이벤트 버스
|
||||||
|
- **특징**: 1:N 관계, 발행자와 구독자의 느슨한 결합
|
||||||
|
|
||||||
|
### 이터레이터 (Iterator)
|
||||||
|
- **목적**: 컬렉션 내부 구조 노출 없이 요소를 순회
|
||||||
|
- **사용 예**: 배열·연결 리스트·트리의 통일된 순회 인터페이스
|
||||||
|
- **특징**: 자료구조 구현과 순회 로직을 분리
|
||||||
|
|
||||||
|
### 전략 (Strategy)
|
||||||
|
- **목적**: 알고리즘을 외부에서 주입받아 런타임에 교체 가능하게 함
|
||||||
|
- **사용 예**: 정렬 알고리즘 선택, 결제 수단 선택, 압축 방식 선택
|
||||||
|
- **특징**: Open-Closed 원칙 준수, 기존 코드 수정 없이 동작 확장
|
||||||
|
|
||||||
|
### 메디에이터 (Mediator)
|
||||||
|
- **목적**: 객체들이 서로 직접 통신하지 않고 중재자를 통해 소통
|
||||||
|
- **사용 예**: Express.js 미들웨어 체인, 채팅방 서버, 항공 관제 시스템
|
||||||
|
- **특징**: 객체 간 결합도 감소, 관계가 M:N → 1:N으로 단순화
|
||||||
|
|
||||||
|
### 스테이트 (State)
|
||||||
|
- **목적**: 객체 내부 상태에 따라 동작이 완전히 달라지게 분리
|
||||||
|
- **사용 예**: 게임 캐릭터 상태 머신, 주문 처리 흐름, UI 컴포넌트 모드
|
||||||
|
- **특징**: 복잡한 `if/switch` 조건문을 상태 클래스로 대체
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 모듈 설계 원칙: 단순하고 테스트하기 쉽게
|
||||||
|
|
||||||
|
> 좋은 코드는 읽기 쉽고, 좋은 모듈은 테스트하기 쉽습니다.
|
||||||
|
> 복잡한 패턴보다 **작고 명확한 단위**로 나누는 것이 먼저입니다.
|
||||||
|
|
||||||
|
### 단순하게 만들기
|
||||||
|
|
||||||
|
- **하나의 모듈 = 하나의 책임**: 파일 하나가 하는 일을 한 문장으로 설명할 수 없다면 쪼개야 함
|
||||||
|
- **의존성은 주입받기**: 모듈 안에서 직접 `new` 하거나 전역 참조 대신, 필요한 객체는 인자로 받기
|
||||||
|
- **외부와의 접점 최소화**: 공개 함수(export)는 꼭 필요한 것만, 나머지는 내부에 숨기기
|
||||||
|
- **설정과 로직 분리**: 환경변수·상수는 별도 파일로, 핵심 로직 안에 하드코딩 금지
|
||||||
|
|
||||||
|
### 테스트하기 쉽게 만들기
|
||||||
|
|
||||||
|
- **순수 함수 우선**: 같은 입력 → 항상 같은 출력, 부작용 없는 함수는 테스트가 단 한 줄
|
||||||
|
- **I/O는 가장자리로**: DB·파일·HTTP 호출은 모듈 경계 바깥으로 밀어내고, 핵심 로직은 순수하게 유지
|
||||||
|
- **인터페이스 기반 설계**: 구체 구현 대신 인터페이스(프로토콜)에 의존하면 테스트 시 Mock으로 쉽게 교체 가능
|
||||||
|
- **작은 단위로 분리**: 함수 하나가 20줄을 넘기 시작하면 쪼갤 신호
|
||||||
|
|
||||||
|
### 체크리스트
|
||||||
|
|
||||||
|
테스트 작성 전, 아래 질문에 "예" 가 되도록 설계하세요.
|
||||||
|
|
||||||
|
| 질문 | 목표 |
|
||||||
|
|------|------|
|
||||||
|
| 이 함수를 Obsidian·Jenkins 없이 단독 실행할 수 있는가? | 외부 의존성 격리 |
|
||||||
|
| Mock 없이 입력값만 바꿔 결과를 검증할 수 있는가? | 순수 함수화 |
|
||||||
|
| 이 모듈의 역할을 한 문장으로 말할 수 있는가? | 단일 책임 |
|
||||||
|
| 추가 기능이 생겼을 때 기존 코드를 수정하지 않아도 되는가? | 확장 가능성 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주의사항
|
||||||
|
|
||||||
|
> 패턴은 도구이지 목적이 아닙니다. 단순한 문제에 복잡한 패턴을 적용하면
|
||||||
|
> 오히려 코드 가독성과 유지보수성이 떨어집니다.
|
||||||
|
|
||||||
|
- **과도한 패턴 남용 금지**: 문제가 패턴을 정당화할 만큼 복잡한지 먼저 판단
|
||||||
|
- **성능 고려**: 프록시·옵저버 등 일부 패턴은 런타임 오버헤드 발생 가능
|
||||||
|
- **팀 이해도 우선**: 팀원 모두가 이해할 수 있는 수준의 패턴을 선택
|
||||||
10
add_obsidian_mcp.sh
Executable file
10
add_obsidian_mcp.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
claude mcp add-json obsidian '{
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["/Users/joungmin/claude/obsidian_mcp_server.py"],
|
||||||
|
"env": {
|
||||||
|
"OBSIDIAN_VAULT": "/Users/joungmin/Documents/Obsidian Vault"
|
||||||
|
}
|
||||||
|
}' --scope project
|
||||||
2
gitea_mcp_server.mjs
Normal file
2
gitea_mcp_server.mjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { runGiteaServer } from "@boringstudio_org/gitea-mcp";
|
||||||
|
runGiteaServer();
|
||||||
30
guide.md
Normal file
30
guide.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 특정 파일/디렉터리 컨텍스트로 시작
|
||||||
|
claude --context ./src
|
||||||
|
|
||||||
|
# 단일 명령 실행 (스크립트 자동화에 유용)
|
||||||
|
claude -p "현재 브랜치의 변경사항을 요약해줘"
|
||||||
|
|
||||||
|
# MCP 서버 목록 확인
|
||||||
|
claude mcp list
|
||||||
|
|
||||||
|
# MCP 서버 추가 (일반)
|
||||||
|
claude mcp add <name> -- <command> [args]
|
||||||
|
|
||||||
|
# JSON 형식으로 MCP 추가 (복잡한 설정 시 권장)
|
||||||
|
claude mcp add-json <name> '<json>'
|
||||||
|
|
||||||
|
# 범위(scope) 지정
|
||||||
|
# --scope local : 현재 프로젝트만 (기본값)
|
||||||
|
# --scope project : .mcp.json에 저장, 팀 공유
|
||||||
|
# --scope user : 모든 프로젝트에서 사용
|
||||||
|
|
||||||
|
## 3. MCP 서버 연동
|
||||||
|
|
||||||
|
### MCP 스코프 이해
|
||||||
|
|
||||||
|
| 스코프 | 저장 위치 | 공유 범위 |
|
||||||
|
|--------|-----------|-----------|
|
||||||
|
| `local` (기본) | `~/.claude.json` | 개인, 현재 프로젝트만 |
|
||||||
|
| `project` | 프로젝트 루트 `.mcp.json` | 팀 전체 공유 (Git 커밋) |
|
||||||
|
| `user` | `~/.claude.json` | 개인, 모든 프로젝트 |
|
||||||
|
|
||||||
161
jenkins_mcp_server.py
Normal file
161
jenkins_mcp_server.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import json, requests
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
from mcp import types
|
||||||
|
import os, base64
|
||||||
|
|
||||||
|
server = Server("jenkins-mcp")
|
||||||
|
JENKINS_URL = os.environ["JENKINS_URL"].rstrip("/")
|
||||||
|
JENKINS_USER = os.environ["JENKINS_USER"]
|
||||||
|
JENKINS_TOKEN = os.environ["JENKINS_TOKEN"]
|
||||||
|
|
||||||
|
def auth():
|
||||||
|
creds = base64.b64encode(f"{JENKINS_USER}:{JENKINS_TOKEN}".encode()).decode()
|
||||||
|
return {"Authorization": f"Basic {creds}"}
|
||||||
|
|
||||||
|
def get_crumb():
|
||||||
|
"""Fetch Jenkins CSRF crumb required for POST/DELETE requests."""
|
||||||
|
r = requests.get(
|
||||||
|
f"{JENKINS_URL}/crumbIssuer/api/json",
|
||||||
|
headers=auth()
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
data = r.json()
|
||||||
|
return {data["crumbRequestField"]: data["crumb"]}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def list_tools():
|
||||||
|
return [
|
||||||
|
types.Tool(name="get_jobs", description="Jenkins 잡 목록 조회",
|
||||||
|
inputSchema={"type": "object", "properties": {}}),
|
||||||
|
types.Tool(name="get_build_status", description="빌드 상태 확인",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string"},
|
||||||
|
"build_number": {"type": "string", "default": "lastBuild"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="trigger_build", description="빌드 트리거",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string"},
|
||||||
|
"parameters": {"type": "object"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="get_build_log", description="빌드 로그 조회",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string"},
|
||||||
|
"build_number": {"type": "string", "default": "lastBuild"},
|
||||||
|
"lines": {"type": "integer", "default": 100}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="create_job", description="새 Jenkins 잡 생성 (XML config 필요)",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name", "config_xml"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string", "description": "생성할 잡 이름"},
|
||||||
|
"config_xml": {"type": "string", "description": "Jenkins job config.xml 내용"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="edit_job", description="기존 Jenkins 잡 설정 수정 (XML config 필요)",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name", "config_xml"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string", "description": "수정할 잡 이름"},
|
||||||
|
"config_xml": {"type": "string", "description": "새 Jenkins job config.xml 내용"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="get_job_config", description="Jenkins 잡의 현재 config.xml 조회",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="delete_job", description="Jenkins 잡 삭제",
|
||||||
|
inputSchema={"type": "object", "required": ["job_name"],
|
||||||
|
"properties": {
|
||||||
|
"job_name": {"type": "string", "description": "삭제할 잡 이름"}
|
||||||
|
}}),
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict):
|
||||||
|
if name == "get_jobs":
|
||||||
|
r = requests.get(
|
||||||
|
f"{JENKINS_URL}/api/json?tree=jobs[name,color,url]",
|
||||||
|
headers=auth()
|
||||||
|
)
|
||||||
|
jobs = r.json().get("jobs", [])
|
||||||
|
return [types.TextContent(type="text", text=json.dumps(jobs, ensure_ascii=False))]
|
||||||
|
|
||||||
|
elif name == "get_build_status":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
num = arguments.get("build_number", "lastBuild")
|
||||||
|
r = requests.get(f"{JENKINS_URL}/job/{job}/{num}/api/json", headers=auth())
|
||||||
|
return [types.TextContent(type="text", text=json.dumps(r.json(), ensure_ascii=False))]
|
||||||
|
|
||||||
|
elif name == "trigger_build":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
params = arguments.get("parameters", {})
|
||||||
|
headers = {**auth(), **get_crumb()}
|
||||||
|
if params:
|
||||||
|
r = requests.post(f"{JENKINS_URL}/job/{job}/buildWithParameters",
|
||||||
|
headers=headers, params=params)
|
||||||
|
else:
|
||||||
|
r = requests.post(f"{JENKINS_URL}/job/{job}/build", headers=headers)
|
||||||
|
return [types.TextContent(type="text", text=f"Status: {r.status_code}")]
|
||||||
|
|
||||||
|
elif name == "get_build_log":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
num = arguments.get("build_number", "lastBuild")
|
||||||
|
lines = arguments.get("lines", 100)
|
||||||
|
r = requests.get(
|
||||||
|
f"{JENKINS_URL}/job/{job}/{num}/logText/progressiveText",
|
||||||
|
headers=auth()
|
||||||
|
)
|
||||||
|
log_lines = r.text.split("\n")[-lines:]
|
||||||
|
return [types.TextContent(type="text", text="\n".join(log_lines))]
|
||||||
|
|
||||||
|
elif name == "get_job_config":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
r = requests.get(f"{JENKINS_URL}/job/{job}/config.xml", headers=auth())
|
||||||
|
if r.status_code == 200:
|
||||||
|
return [types.TextContent(type="text", text=r.text)]
|
||||||
|
return [types.TextContent(type="text", text=f"Error {r.status_code}: {r.text}")]
|
||||||
|
|
||||||
|
elif name == "create_job":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
config_xml = arguments["config_xml"]
|
||||||
|
headers = {**auth(), **get_crumb(), "Content-Type": "application/xml"}
|
||||||
|
r = requests.post(
|
||||||
|
f"{JENKINS_URL}/createItem?name={job}",
|
||||||
|
headers=headers,
|
||||||
|
data=config_xml.encode("utf-8")
|
||||||
|
)
|
||||||
|
if r.status_code in (200, 201):
|
||||||
|
return [types.TextContent(type="text", text=f"Job '{job}' created successfully.")]
|
||||||
|
return [types.TextContent(type="text", text=f"Error {r.status_code}: {r.text}")]
|
||||||
|
|
||||||
|
elif name == "edit_job":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
config_xml = arguments["config_xml"]
|
||||||
|
headers = {**auth(), **get_crumb(), "Content-Type": "application/xml"}
|
||||||
|
r = requests.post(
|
||||||
|
f"{JENKINS_URL}/job/{job}/config.xml",
|
||||||
|
headers=headers,
|
||||||
|
data=config_xml.encode("utf-8")
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
return [types.TextContent(type="text", text=f"Job '{job}' updated successfully.")]
|
||||||
|
return [types.TextContent(type="text", text=f"Error {r.status_code}: {r.text}")]
|
||||||
|
|
||||||
|
elif name == "delete_job":
|
||||||
|
job = arguments["job_name"]
|
||||||
|
headers = {**auth(), **get_crumb()}
|
||||||
|
r = requests.post(f"{JENKINS_URL}/job/{job}/doDelete", headers=headers)
|
||||||
|
if r.status_code in (200, 302):
|
||||||
|
return [types.TextContent(type="text", text=f"Job '{job}' deleted successfully.")]
|
||||||
|
return [types.TextContent(type="text", text=f"Error {r.status_code}: {r.text}")]
|
||||||
|
|
||||||
|
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import asyncio
|
||||||
|
async def main():
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||||
|
asyncio.run(main())
|
||||||
168
obsidian_mcp_server.py
Normal file
168
obsidian_mcp_server.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import json, os, glob, shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
from mcp import types
|
||||||
|
|
||||||
|
server = Server("obsidian-mcp")
|
||||||
|
VAULT = Path(os.environ.get("OBSIDIAN_VAULT", "/Users/joungmin/Documents/Obsidian Vault"))
|
||||||
|
|
||||||
|
def note_path(relative: str) -> Path:
|
||||||
|
"""Resolve a relative note path inside the vault. Adds .md if missing."""
|
||||||
|
p = VAULT / relative
|
||||||
|
if not p.suffix:
|
||||||
|
p = p.with_suffix(".md")
|
||||||
|
return p
|
||||||
|
|
||||||
|
def rel(path: Path) -> str:
|
||||||
|
return str(path.relative_to(VAULT))
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def list_tools():
|
||||||
|
return [
|
||||||
|
types.Tool(name="list_folder", description="폴더 내 파일/폴더 목록 조회",
|
||||||
|
inputSchema={"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder": {"type": "string", "description": "상대 경로 (비어두면 vault 루트)"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="search_notes", description="노트 내용 전체 텍스트 검색",
|
||||||
|
inputSchema={"type": "object", "required": ["query"],
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string", "description": "검색 키워드"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="get_note", description="노트 내용 읽기",
|
||||||
|
inputSchema={"type": "object", "required": ["path"],
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string", "description": "노트 상대 경로 (.md 생략 가능)"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="create_note", description="노트 생성 또는 덮어쓰기",
|
||||||
|
inputSchema={"type": "object", "required": ["path", "content"],
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"content": {"type": "string"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="append_note", description="기존 노트 끝에 내용 추가",
|
||||||
|
inputSchema={"type": "object", "required": ["path", "content"],
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"content": {"type": "string"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="delete_note", description="노트 삭제",
|
||||||
|
inputSchema={"type": "object", "required": ["path"],
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="move_note", description="노트 이동 또는 이름 변경",
|
||||||
|
inputSchema={"type": "object", "required": ["src", "dst"],
|
||||||
|
"properties": {
|
||||||
|
"src": {"type": "string", "description": "원본 상대 경로"},
|
||||||
|
"dst": {"type": "string", "description": "대상 상대 경로"}
|
||||||
|
}}),
|
||||||
|
types.Tool(name="create_daily_note", description="오늘의 Daily Note 생성 (이미 있으면 내용 반환)",
|
||||||
|
inputSchema={"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"extra_content": {"type": "string", "description": "추가할 내용 (선택)"}
|
||||||
|
}}),
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict):
|
||||||
|
|
||||||
|
if name == "list_folder":
|
||||||
|
folder = arguments.get("folder", "").strip("/")
|
||||||
|
target = VAULT / folder if folder else VAULT
|
||||||
|
if not target.exists():
|
||||||
|
return [types.TextContent(type="text", text=f"폴더 없음: {folder}")]
|
||||||
|
items = []
|
||||||
|
for p in sorted(target.iterdir()):
|
||||||
|
if p.name.startswith("."):
|
||||||
|
continue
|
||||||
|
items.append({
|
||||||
|
"name": p.name,
|
||||||
|
"type": "folder" if p.is_dir() else "file",
|
||||||
|
"path": rel(p)
|
||||||
|
})
|
||||||
|
return [types.TextContent(type="text", text=json.dumps(items, ensure_ascii=False, indent=2))]
|
||||||
|
|
||||||
|
elif name == "search_notes":
|
||||||
|
query = arguments["query"].lower()
|
||||||
|
results = []
|
||||||
|
for md in VAULT.rglob("*.md"):
|
||||||
|
try:
|
||||||
|
text = md.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
if query in text.lower() or query in md.name.lower():
|
||||||
|
# Extract surrounding context
|
||||||
|
lines = text.splitlines()
|
||||||
|
matched = [l for l in lines if query in l.lower()]
|
||||||
|
results.append({
|
||||||
|
"path": rel(md),
|
||||||
|
"matches": matched[:3]
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return [types.TextContent(type="text",
|
||||||
|
text=json.dumps(results, ensure_ascii=False, indent=2)
|
||||||
|
if results else "검색 결과 없음")]
|
||||||
|
|
||||||
|
elif name == "get_note":
|
||||||
|
p = note_path(arguments["path"])
|
||||||
|
if not p.exists():
|
||||||
|
return [types.TextContent(type="text", text=f"노트 없음: {arguments['path']}")]
|
||||||
|
return [types.TextContent(type="text", text=p.read_text(encoding="utf-8"))]
|
||||||
|
|
||||||
|
elif name == "create_note":
|
||||||
|
p = note_path(arguments["path"])
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(arguments["content"], encoding="utf-8")
|
||||||
|
return [types.TextContent(type="text", text=f"저장됨: {rel(p)}")]
|
||||||
|
|
||||||
|
elif name == "append_note":
|
||||||
|
p = note_path(arguments["path"])
|
||||||
|
if not p.exists():
|
||||||
|
return [types.TextContent(type="text", text=f"노트 없음: {arguments['path']}")]
|
||||||
|
with p.open("a", encoding="utf-8") as f:
|
||||||
|
f.write("\n" + arguments["content"])
|
||||||
|
return [types.TextContent(type="text", text=f"추가됨: {rel(p)}")]
|
||||||
|
|
||||||
|
elif name == "delete_note":
|
||||||
|
p = note_path(arguments["path"])
|
||||||
|
if not p.exists():
|
||||||
|
return [types.TextContent(type="text", text=f"노트 없음: {arguments['path']}")]
|
||||||
|
p.unlink()
|
||||||
|
return [types.TextContent(type="text", text=f"삭제됨: {rel(p)}")]
|
||||||
|
|
||||||
|
elif name == "move_note":
|
||||||
|
src = note_path(arguments["src"])
|
||||||
|
dst = note_path(arguments["dst"])
|
||||||
|
if not src.exists():
|
||||||
|
return [types.TextContent(type="text", text=f"원본 없음: {arguments['src']}")]
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.move(str(src), str(dst))
|
||||||
|
return [types.TextContent(type="text", text=f"이동됨: {rel(src)} → {rel(dst)}")]
|
||||||
|
|
||||||
|
elif name == "create_daily_note":
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
p = VAULT / "Daily Notes" / f"{today}.md"
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
extra = arguments.get("extra_content", "")
|
||||||
|
if p.exists():
|
||||||
|
if extra:
|
||||||
|
with p.open("a", encoding="utf-8") as f:
|
||||||
|
f.write("\n" + extra)
|
||||||
|
return [types.TextContent(type="text", text=f"기존 노트에 추가됨: {rel(p)}")]
|
||||||
|
return [types.TextContent(type="text", text=p.read_text(encoding="utf-8"))]
|
||||||
|
content = f"# {today}\n\n{extra}" if extra else f"# {today}\n\n"
|
||||||
|
p.write_text(content, encoding="utf-8")
|
||||||
|
return [types.TextContent(type="text", text=f"생성됨: {rel(p)}")]
|
||||||
|
|
||||||
|
return [types.TextContent(type="text", text=f"알 수 없는 도구: {name}")]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@boringstudio_org/gitea-mcp": "^1.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
setup_mcp.sh
Executable file
76
setup_mcp.sh
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# setup_mcp.sh
|
||||||
|
# .env를 읽어 .mcp.json을 생성합니다.
|
||||||
|
# 사용법: ./setup_mcp.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ENV_FILE="$(dirname "$0")/.env"
|
||||||
|
MCP_FILE="$(dirname "$0")/.mcp.json"
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "오류: .env 파일이 없습니다."
|
||||||
|
echo " cp .env.example .env 후 값을 채워주세요."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# .env 로드 (주석·빈 줄 제외)
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
[[ "$key" =~ ^\s*# ]] && continue
|
||||||
|
[[ -z "$key" ]] && continue
|
||||||
|
export "$key=$value"
|
||||||
|
done < <(grep -v '^\s*#' "$ENV_FILE" | grep -v '^\s*$')
|
||||||
|
|
||||||
|
# 필수 변수 확인
|
||||||
|
REQUIRED=(REDMINE_URL REDMINE_API_KEY JENKINS_URL JENKINS_USER JENKINS_TOKEN GITEA_API_URL GITEA_TOKEN OBSIDIAN_VAULT)
|
||||||
|
for VAR in "${REQUIRED[@]}"; do
|
||||||
|
if [ -z "${!VAR}" ]; then
|
||||||
|
echo "오류: $VAR 값이 비어 있습니다. .env를 확인하세요."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cat > "$MCP_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"redmine": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@onozaty/redmine-mcp-server"],
|
||||||
|
"env": {
|
||||||
|
"REDMINE_URL": "${REDMINE_URL}",
|
||||||
|
"REDMINE_API_KEY": "${REDMINE_API_KEY}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jenkins": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "/opt/homebrew/bin/python3.10",
|
||||||
|
"args": ["/Users/$(whoami)/claude/jenkins_mcp_server.py"],
|
||||||
|
"env": {
|
||||||
|
"JENKINS_URL": "${JENKINS_URL}",
|
||||||
|
"JENKINS_USER": "${JENKINS_USER}",
|
||||||
|
"JENKINS_TOKEN": "${JENKINS_TOKEN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitea": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/Users/$(whoami)/claude/gitea_mcp_server.mjs"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_API_URL": "${GITEA_API_URL}",
|
||||||
|
"GITEA_TOKEN": "${GITEA_TOKEN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obsidian": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "/opt/homebrew/bin/python3.10",
|
||||||
|
"args": ["/Users/$(whoami)/claude/obsidian_mcp_server.py"],
|
||||||
|
"env": {
|
||||||
|
"OBSIDIAN_VAULT": "${OBSIDIAN_VAULT}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ".mcp.json 생성 완료. Claude Code를 재시작하세요."
|
||||||
Reference in New Issue
Block a user