본문 바로가기
AWS Ambassador

Amazon Q와 Confluence 완벽 연동 가이드: AI로 문서 작업 자동화하기

by 백룡화검 2025. 7. 18.

개발자 또는 IT실무 종사자에게 문서 작업은 때로는 코드 작성만큼이나 많은 시간과 노력이 필요한 일입니다. 특히 팀 단위로 지식을 관리하는 Confluence는 매우 강력한 도구이지만, 반복적인 페이지 생성, 검색, 업데이트 작업은 생산성을 저해하는 요인이 되기도 합니다. 만약 터미널에서 대화형 AI를 통해 이런 Confluence 작업을 처리할 수 있다면 어떨까요?

이 글에서는 AWS의 생성형 AI기반 어시스턴트인 Amazon Q DeveloperModel Context Protocol(MCP) 서버와 연동하여 Confluence의 문서 관리 작업을 자동화하는 방법을 소개합니다. 이 가이드를 통해 여러분은 터미널을 벗어나지 않고도 Confluence 페이지를 자유자재로 다룰 수 있게 될 것입니다.


Amazon Q Developer와 MCP, 왜 함께 사용해야 할까?

Amazon Q Developer는 코드 작성, 디버깅, 테스트 등 개발의 전 과정에 도움을 주는 AI 어시스턴트입니다. 하지만 Amazon Q의 진정한 힘은 단순히 내장된 기능을 사용하는 것을 넘어, 외부 도구와 연동하여 그 능력을 확장할 때 발휘됩니다.

이때 핵심적인 역할을 하는 것이 바로 Model Context Protocol(MCP)입니다. MCP는 Amazon Q가 외부 시스템의 API나 도구를 이해하고 사용할 수 있도록 연결해주는 일종의 '통역사'입니다. 우리는 Confluence API를 제어하는 간단한 MCP 서버를 직접 구축함으로써, Amazon Q에게 "Confluence에 새로운 페이지를 만들어줘" 또는 "특정 키워드가 포함된 문서를 찾아줘"와 같은 명령을 내릴 수 있게 됩니다.

이 조합은 개발 워크플로우를 혁신적으로 바꿀 수 있습니다. 코드에 대한 문서를 작성하거나, 프로젝트 기획 문서를 업데이트하는 등의 작업을 IDE나 터미널에서 곧바로 처리하여 컨텍스트 전환에 드는 비용을 획기적으로 줄여줍니다.

전체 아키텍처

이 연동 방식의 전체적인 구조는 다음과 같습니다. 개발자가 Amazon Q Developer CLI에 명령을 내리면, CLI는 로컬에서 실행 중인 MCP 서버를 호출합니다. MCP 서버는 이 명령을 해석하여 Confluence API가 이해할 수 있는 요청으로 변환한 후 Atlassian Cloud로 전송합니다. Confluence는 요청을 처리하고 그 결과를 다시 MCP 서버와 Q CLI를 거쳐 개발자에게 보여줍니다.

 

다이어그램 설명

  1. 개발자 (사용자 환경): 개발자가 터미널에서 q chat과 같은 명령어를 입력하여 Amazon Q Developer CLI와 상호작용합니다.
  2. Amazon Q Developer CLI (사용자 환경): 사용자의 자연어 명령을 받아 로컬에서 실행 중인 MCP 서버에 JSON-RPC 형식으로 요청을 보냅니다.
  3. MCP 서버 (로컬 MCP 실행 환경): Q CLI로부터 받은 요청을 해석합니다. 이 서버는 uvx를 통해 파이썬 스크립트(mcp_atlassian.py)로 구현되어 있습니다.
  4. Confluence API (Atlassian 클라우드): MCP 서버는 해석된 명령을 Confluence가 이해할 수 있는 REST API 요청으로 변환하여 Atlassian 클라우드로 전송합니다.
  5. 결과 반환: Confluence API는 요청 처리 결과를 다시 MCP 서버로 보내고, 최종적으로 Amazon Q CLI를 통해 개발자의 터미널 화면에 결과가 표시됩니다.

1. 사전 준비사항

프로젝트를 시작하기 전에 필요한 소프트웨어와 계정 정보를 준비해야 합니다.

필수 소프트웨어

  • Python 3.10 이상: MCP 서버는 Python 3.10 이상을 필요로 합니다.
  • UV: 빠른 Python 패키지 관리 및 스크립트 실행 도구입니다.
  • Amazon Q Developer CLI: MCP 서버와 통신할 주체입니다.

소프트웨어 설치

아래 명령어를 통해 uvAmazon Q Developer CLI를 설치합니다.

  • UV설치
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# pip으로 설치
pip install uv
  • Amazon Q Developer CLI설치
# AWS CLI 설치 후
pip install amazon-q-developer-cli

# 또는 공식 설치 방법 사용
# https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/cli-install.html

Atlassian 계정 정보

  • Confluence URL: 사용 중인 Confluence의 주소
  • 이메일 주소: Atlassian 계정에 사용하는 이메일
  • API 토큰: Atlassian 계정 설정에서 생성한 API 인증 토큰
  • 스페이스 키: 작업 대상이 될 Confluence 스페이스의 고유 키

2. 전체 설정 단계

이제 본격적으로 연동을 위한 환경 설정을 시작하겠습니다. 프로젝트 디렉토리 생성부터 서버 코드 작성까지 총 5단계로 진행됩니다.

1단계: 프로젝트 디렉토리 생성

터미널에서 아래 명령어를 실행하여 프로젝트를 시작할 작업 공간을 만듭니다.

mkdir confluence-mcp
cd confluence-mcp


2단계: Atlassian API 토큰발급

Atlassian API 토큰페이지에 접속하여 API 토큰만들기 기능을 이용해서 Confluence연동시에 사용할 API 토큰을 생성합니다.

▲ 중요: API 토큰은 생성될때 1회만 표시되기 때문에, 반드시 안전한 곳에 저장하셔야 합니다.


3단계: 환경변수 파일 .env 생성

프로젝트 루트에 .env 파일을 생성합니다. 이 파일은 API 토큰과 같은 민감한 정보를 코드와 분리하여 안전하게 보관하는 역할을 합니다.

.env 파일 내용:

# Confluence 설정 (필수)
CONFLUENCE_EMAIL="your-email@company.com"
CONFLUENCE_URL="https://your-company.atlassian.net"
CONFLUENCE_API_TOKEN="your_api_token_here"
CONFLUENCE_SPACE_KEY="YOUR_SPACE"
PARENT_PAGE_ID="your_parent_page_id"

# Jira 설정 (선택사항 - 필요한 경우 주석 해제하고 설정)
#JIRA_EMAIL="your-email@company.com"
#JIRA_URL="https://your-company.atlassian.net"
#JIRA_API_TOKEN="your_jira_api_token"

# MCP 서버 설정
MCP_VERBOSE=true

보안 경고: .env 파일은 민감한 정보를 포함하므로 절대 Git에 커밋해서는 안 됩니다. .gitignore 파일에 .env를 추가하는 것을 잊지 마세요.

3단계: 프로젝트 의존성 파일 pyproject.toml 생성

프로젝트 루트에 pyproject.toml 파일을 생성하여 이 프로젝트가 필요로 하는 Python 패키지들을 정의합니다.

pyproject.toml 파일 내용:

[project]
name = "mcp-atlassian"
version = "0.1.0"
description = "MCP Atlassian server for Confluence and Jira stdio only"
requires-python = ">=3.10"
dependencies = [
    "atlassian-python-api>=4.0.4",
    "requests[socks]>=2.32.4",
    "beautifulsoup4>=4.13.4",
    "httpx>=0.28.1",
    "mcp>=1.11.0,<2.0.0",
    "python-dotenv>=1.1.1",
    "markdownify>=1.1.0",
    "markdown>=3.8.2"
]

[project.scripts]
mcp-atlassian-server = "mcp_atlassian:run_main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

4단계: 핵심 서버 코드 mcp_atlassian.py 작성

이제 가장 중요한 MCP 서버 코드를 작성합니다. 제공된 문서들을 모두 취합한 전체 코드는 아래와 같습니다. 코드 하단에서 각 부분의 역할을 자세히 설명합니다.

mcp_atlassian.py 전체 소스 코드:

#!/usr/bin/env python3
# MCP Atlassian Server stdio version
# Confluence와 Jira를 위한 Model Context Protocol 서버

import os
import sys
import logging
import asyncio
import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
from datetime import datetime

# 서드파티 라이브러리 임포트
from dotenv import load_dotenv
from atlassian import Confluence, Jira
import requests
import base64

# MCP 라이브러리 임포트
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# --- Step 1: 헤더 및 초기 설정 ---
# .env 파일 로드
script_dir = Path(__file__).parent
env_path = script_dir / ".env"
if env_path.exists():
    load_dotenv(dotenv_path=env_path)
    if os.getenv("MCP_VERBOSE", "false").lower() == "true":
        print(f"[MCP] .env 파일 로드됨: {env_path}", file=sys.stderr)
else:
    print(f"[MCP] 경고: .env 파일을 찾을 수 없습니다: {env_path}", file=sys.stderr)

# 로깅 설정
logging.basicConfig(
    level=logging.ERROR, # stdio 간섭을 최소화하기 위해 ERROR 레벨만 출력
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger(__name__)

# MCP 서버 및 Atlassian 클라이언트 전역 변수 초기화
server = Server("mcp-atlassian")
confluence: Optional[Confluence] = None
jira: Optional[Jira] = None

# --- Step 2: 클라이언트 초기화 함수 ---
def init_confluence():
    """Confluence 클라이언트 초기화"""
    global confluence
    confluence_url = os.getenv("CONFLUENCE_URL")
    confluence_email = os.getenv("CONFLUENCE_EMAIL")
    confluence_token = os.getenv("CONFLUENCE_API_TOKEN")
    if confluence_url and confluence_email and confluence_token:
        try:
            if not confluence_url.endswith('/wiki') and '/wiki' not in confluence_url:
                confluence_url = confluence_url.rstrip('/') + '/wiki'
            
            confluence = Confluence(
                url=confluence_url,
                username=confluence_email,
                password=confluence_token,
                cloud=True
            )
            logger.info("Confluence 클라이언트가 성공적으로 초기화되었습니다.")
        except Exception as e:
            logger.error(f"Confluence 초기화 실패: {e}")

def init_jira():
    """Jira 클라이언트 초기화 (선택사항)"""
    global jira
    jira_url = os.getenv("JIRA_URL")
    jira_email = os.getenv("JIRA_EMAIL")
    jira_token = os.getenv("JIRA_API_TOKEN")
    if jira_url and jira_email and jira_token:
        try:
            jira = Jira(
                url=jira_url,
                username=jira_email,
                password=jira_token,
                cloud=True
            )
            logger.info("Jira 클라이언트가 성공적으로 초기화되었습니다.")
        except Exception as e:
            logger.error(f"Jira 초기화 실패: {e}")

# --- Step 3: 도구 목록 정의 ---
@server.list_tools()
async def list_tools() -> List[Tool]:
    """사용 가능한 도구 목록을 반환합니다."""
    tools = [
        Tool(
            name="confluence_search",
            description="Confluence에서 페이지를 검색합니다.",
            input_schema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "검색할 키워드"},
                    "space_key": {"type": "string", "description": "검색할 스페이스 키 (선택사항)"},
                    "limit": {"type": "integer", "description": "결과 개수 제한", "default": 10}
                },
                "required": ["query"]
            }
        ),
        Tool(
            name="confluence_get_page",
            description="특정 Confluence 페이지의 내용을 조회합니다.",
            input_schema={
                "type": "object",
                "properties": {
                    "page_id": {"type": "string", "description": "페이지 ID"},
                    "expand": {"type": "string", "description": "확장할 필드 (body.storage, version 등)", "default": "body.storage,version"}
                },
                "required": ["page_id"]
            }
        ),
        Tool(
            name="confluence_create_page",
            description="새로운 Confluence 페이지를 생성합니다.",
            input_schema={
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "페이지 제목"},
                    "content": {"type": "string", "description": "페이지 내용"},
                    "space_key": {"type": "string", "description": "스페이스 키"},
                    "parent_id": {"type": "string", "description": "부모 페이지 ID (선택사항)"},
                    "content_format": {"type": "string", "description": "내용 형식 (storage 또는 markdown)", "default": "storage"}
                },
                "required": ["title", "content", "space_key"]
            }
        ),
        Tool(
            name="confluence_update_page",
            description="기존 Confluence 페이지를 업데이트합니다.",
            input_schema={
                "type": "object",
                "properties": {
                    "page_id": {"type": "string", "description": "페이지 ID"},
                    "title": {"type": "string", "description": "페이지 제목"},
                    "content": {"type": "string", "description": "페이지 내용"},
                    "content_format": {"type": "string", "description": "내용 형식 (storage 또는 markdown)", "default": "storage"}
                },
                "required": ["page_id", "title", "content"]
            }
        )
    ]
    if jira:
        tools.extend([
            Tool(
                name="jira_search",
                description="JQL을 사용하여 Jira 이슈를 검색합니다.",
                input_schema={
                    "type": "object",
                    "properties": {
                        "jql": {"type": "string", "description": "JQL 쿼리"},
                        "limit": {"type": "integer", "description": "결과 개수 제한", "default": 10}
                    },
                    "required": ["jql"]
                }
            ),
            Tool(
                name="jira_get_issue",
                description="특정 Jira 이슈의 상세 정보를 가져옵니다.",
                input_schema={
                    "type": "object",
                    "properties": {
                        "issue_key": {"type": "string", "description": "이슈 키 (예: PROJ-123)"}
                    },
                    "required": ["issue_key"]
                }
            )
        ])
    return tools

# --- Step 4: 도구 호출 라우터 ---
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """도구 호출을 처리합니다."""
    if name == "confluence_search":
        return await confluence_search_tool(arguments)
    elif name == "confluence_get_page":
        return await confluence_get_page_tool(arguments)
    elif name == "confluence_create_page":
        return await confluence_create_page_tool(arguments)
    elif name == "confluence_update_page":
        return await confluence_update_page_tool(arguments)
    elif name == "jira_search":
        return await jira_search_tool(arguments)
    elif name == "jira_get_issue":
        return await jira_get_issue_tool(arguments)
    else:
        return [TextContent(type="text", text=f"알 수 없는 도구: {name}")]

# --- Step 5: Confluence 검색 도구 ---
async def confluence_search_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Confluence 검색 도구"""
    if not confluence:
        return [TextContent(type="text", text="Confluence가 초기화되지 않았습니다.")]
    try:
        query = arguments["query"]
        space_key = arguments.get("space_key")
        limit = arguments.get("limit", 10)
        cql = f'text ~ "{query}"'
        if space_key:
            cql += f' AND space = "{space_key}"'
        results = confluence.cql(cql, limit=limit)
        pages = []
        for result in results.get('results', []):
            page_info = {
                'id': result.get('content', {}).get('id'),
                'title': result.get('content', {}).get('title'),
                'type': result.get('content', {}).get('type'),
                'space': result.get('content', {}).get('space', {}).get('key'),
                'url': result.get('url'),
                'excerpt': result.get('excerpt', "")
            }
            pages.append(page_info)
        result_data = {
            "total": results.get('totalSize', 0),
            "pages": pages
        }
        return [TextContent(type="text", text=json.dumps(result_data, ensure_ascii=False, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"검색 중 오류 발생: {str(e)}")]

# --- Step 6: Confluence 페이지 조회 도구 ---
async def confluence_get_page_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Confluence 페이지 조회 도구"""
    if not confluence:
        return [TextContent(type="text", text="Confluence가 초기화되지 않았습니다.")]
    try:
        page_id = arguments["page_id"]
        expand = arguments.get("expand", "body.storage,version")
        page = confluence.get_page_by_id(page_id, expand=expand)
        page_info = {
            'id': page.get('id'),
            'title': page.get('title'),
            'type': page.get('type'),
            'space': page.get('space', {}).get('key'),
            'version': page.get('version', {}).get('number'),
            'content': page.get('body', {}).get('storage', {}).get('value', ""),
            'url': page.get('_links', {}).get('webui', "")
        }
        return [TextContent(type="text", text=json.dumps(page_info, ensure_ascii=False, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"페이지 조회 중 오류 발생: {str(e)}")]

# --- Step 7: Confluence 페이지 생성 도구 ---
async def confluence_create_page_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Confluence 페이지 생성 도구"""
    if not confluence:
        return [TextContent(type="text", text="Confluence가 초기화되지 않았습니다.")]
    try:
        title = arguments["title"]
        content = arguments["content"]
        space_key = arguments["space_key"]
        parent_id = arguments.get("parent_id")
        
        if os.getenv("MCP_VERBOSE", "false").lower() == "true":
            print(f"[MCP] 페이지 생성 시도: {title} in {space_key}", file=sys.stderr)

        # REST API 직접 사용
        confluence_url = os.getenv("CONFLUENCE_URL")
        confluence_email = os.getenv("CONFLUENCE_EMAIL")
        confluence_token = os.getenv("CONFLUENCE_API_TOKEN")

        if not confluence_url.endswith("/wiki") and '/wiki' not in confluence_url:
            confluence_url = confluence_url.rstrip('/') + '/wiki'
        
        api_url = f"{confluence_url}/rest/api/content"
        
        auth_string = f"{confluence_email}:{confluence_token}"
        auth_bytes = auth_string.encode('ascii')
        auth_b64 = base64.b64encode(auth_bytes).decode('ascii')

        headers = {
            'Authorization': f'Basic {auth_b64}',
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }

        page_data = {
            "type": "page",
            "title": title,
            "space": {"key": space_key},
            "body": {
                "storage": {
                    "value": content,
                    "representation": "storage"
                }
            }
        }
        if parent_id:
            page_data["ancestors"] = [{"id": parent_id}]

        response = requests.post(api_url, headers=headers, json=page_data)

        if response.status_code == 200:
            result = response.json()
            page_info = {
                'id': result.get('id'),
                'title': result.get('title'),
                'space': result.get('space', {}).get('key'),
                'url': result.get('_links', {}).get('webui', ""),
                'status': 'created'
            }
            return [TextContent(type="text", text=json.dumps(page_info, ensure_ascii=False, indent=2))]
        else:
            error_msg = f"API 호출 실패: {response.status_code} {response.text}"
            print(f"[MCP] {error_msg}", file=sys.stderr)
            return [TextContent(type="text", text=error_msg)]
    except Exception as e:
        error_msg = f"페이지 생성 중 오류 발생: {str(e)}"
        print(f"[MCP] {error_msg}", file=sys.stderr)
        return [TextContent(type="text", text=error_msg)]

# --- Step 8: Confluence 페이지 업데이트 도구 (수정됨) ---
async def confluence_update_page_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Confluence 페이지 업데이트 도구"""
    if not confluence:
        return [TextContent(type="text", text="Confluence가 초기화되지 않았습니다.")]
    try:
        page_id = arguments["page_id"]
        title = arguments["title"]
        content = arguments["content"]
        
        result = confluence.update_page(
            page_id=page_id,
            title=title,
            body=content
        )
        page_info = {
            "id": result.get('id'),
            'title': result.get('title'),
            'version': result.get('version', {}).get('number'),
            'url': result.get('_links', {}).get('webui', ""),
            'status': 'updated'
        }
        return [TextContent(type="text", text=json.dumps(page_info, ensure_ascii=False, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"페이지 업데이트 중 오류 발생: {str(e)}")]

# --- Step 9: Jira 도구들 (선택사항) ---
async def jira_search_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Jira 검색 도구"""
    if not jira:
        return [TextContent(type="text", text="Jira가 초기화되지 않았습니다.")]
    try:
        jql = arguments["jql"]
        limit = arguments.get("limit", 10)
        results = jira.jql(jql, limit=limit)
        issues = []
        for issue in results.get('issues', []):
            fields = issue.get('fields', {})
            issue_info = {
                'key': issue.get('key'),
                'summary': fields.get('summary'),
                'status': fields.get('status', {}).get('name'),
                'assignee': fields.get('assignee', {}).get('displayName') if fields.get('assignee') else None,
                'priority': fields.get('priority', {}).get('name') if fields.get('priority') else None,
                'issue_type': fields.get('issuetype', {}).get('name'),
            }
            issues.append(issue_info)
        result_data = {
            "total": results.get('total', 0),
            "issues": issues
        }
        return [TextContent(type="text", text=json.dumps(result_data, ensure_ascii=False, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"Jira 검색 중 오류 발생: {str(e)}")]

async def jira_get_issue_tool(arguments: Dict[str, Any]) -> Sequence[TextContent]:
    """Jira 이슈 조회 도구"""
    if not jira:
        return [TextContent(type="text", text="Jira가 초기화되지 않았습니다.")]
    try:
        issue_key = arguments["issue_key"]
        issue = jira.issue(issue_key)
        fields = issue.get('fields', {})
        issue_info = {
            'key': issue.get('key'),
            'summary': fields.get('summary'),
            'description': fields.get('description'),
            'status': fields.get('status', {}).get('name'),
            'assignee': fields.get('assignee', {}).get('displayName') if fields.get('assignee') else None,
            'reporter': fields.get('reporter', {}).get('displayName'),
            'priority': fields.get('priority', {}).get('name') if fields.get('priority') else None,
        }
        return [TextContent(type="text", text=json.dumps(issue_info, ensure_ascii=False, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"Jira 이슈 조회 중 오류 발생: {str(e)}")]

# --- Step 10: 메인 함수 (최종) ---
async def main():
    """메인 함수 - stdio 모드로 MCP 서버 실행"""
    if os.getenv("MCP_VERBOSE", "false").lower() == "true":
        print("[MCP] MCP Atlassian 서버 시작 중...", file=sys.stderr)
    
    init_confluence()
    init_jira()
    
    if os.getenv("MCP_VERBOSE", "false").lower() == "true":
        print(f"[MCP] Confluence 초기화: {'성공' if confluence else '실패'}", file=sys.stderr)
        print(f"[MCP] Jira 초기화: {'성공' if jira else '실패'}", file=sys.stderr)
        
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())

def run_main():
    """동기 래퍼 함수 - uvx 스크립트 실행용"""
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        if os.getenv("MCP_VERBOSE", "false").lower() == "true":
            print("\n[MCP] 서버 종료.", file=sys.stderr)
    except Exception as e:
        print(f"[MCP] 서버 실행 중 오류 발생: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    run_main()

소스 코드 설명

  • Step 1: 헤더 및 초기 설정스크립트 실행에 필요한 기본 모듈(os, sys, asyncio 등)과 서드파티 라이브러리(dotenv, atlassian, mcp 등)를 가져옵니다. .env 파일을 로드하여 환경변수를 설정하고, 로깅 기본 설정을 마칩니다.
  • Step 2: 클라이언트 초기화 함수init_confluence()init_jira() 함수는 .env 파일에 입력된 URL, 이메일, API 토큰 정보를 사용하여 Confluence와 Jira API 클라이언트 객체를 생성하고 초기화합니다.
  • Step 3: 도구 목록 정의@server.list_tools() 데코레이터를 사용한 list_tools 함수입니다. 이 함수는 Amazon Q에게 이 서버가 어떤 도구들(예: confluence_search, confluence_create_page)을 제공하는지, 그리고 각 도구를 사용하려면 어떤 인자(파라미터)가 필요한지를 알려주는 역할을 합니다.
  • Step 4: 도구 호출 라우터@server.call_tool() 데코레이터를 사용한 call_tool 함수입니다. Amazon Q가 특정 도구를 사용하겠다고 요청하면, 이 함수가 요청된 도구 이름(name)에 따라 적절한 실제 처리 함수(예: confluence_search_tool)를 호출해주는 길잡이 역할을 합니다.
  • Step 5: Confluence 검색 도구confluence_search_tool 함수는 Confluence 페이지 검색 요청을 실제로 처리하는 로직을 담고 있습니다. CQL(Confluence Query Language)을 구성하여 API를 호출하고, 검색 결과를 JSON 형식으로 반환합니다.
  • Step 6: Confluence 페이지 조회 도구confluence_get_page_tool 함수는 특정 페이지 ID를 받아 해당 페이지의 상세 내용을 조회하는 역할을 합니다. expand 파라미터를 통해 본문, 버전 정보 등 추가적인 정보를 함께 가져올 수 있습니다.
  • Step 7: Confluence 페이지 생성 도구confluence_create_page_tool 함수는 새로운 Confluence 페이지를 생성하는 로직을 담당합니다. 이 함수는 `atlassian-python-api` 라이브러리 대신 `requests` 모듈을 직접 사용하여 REST API를 호출하는 방식으로 구현되어 있습니다.
  • Step 8: Confluence 페이지 업데이트 도구confluence_update_page_tool 함수는 기존 페이지의 제목이나 내용을 수정하는 역할을 합니다.
  • Step 9: Jira 도구들 (선택사항)jira_search_tool, jira_get_issue_tool 함수들입니다. .env 파일에 Jira 정보가 설정된 경우에만 활성화되며, JQL 검색 및 특정 이슈 조회 기능을 제공합니다.
  • Step 10: 메인 함수 (최종)스크립트의 메인 진입점입니다. main 함수는 정의된 클라이언트들을 초기화하고 MCP 서버를 시작합니다. run_main 함수와 if __name__ == "__main__": 구문은 이 비동기 코드를 동기적으로 실행할 수 있게 해주어 uvx와 같은 실행 도구에서 쉽게 사용할 수 있도록 합니다.

5단계: Amazon Q CLI 설정

마지막으로, Amazon Q CLI가 우리가 만든 MCP 서버를 인식하고 실행할 수 있도록 설정 파일을 생성합니다.

사용자의 홈 디렉토리 아래 ~/.aws/amazonq/mcp.json 경로에 파일을 만들고 아래 내용을 작성하세요.

{
  "mcpServers": {
    "mcp_atlassian": {
      "command": "uvx",
      "args": [
        "--from",
        "/path/to/your/confluence-mcp",
        "--with-editable",
        "python",
        "mcp_atlassian.py"
      ],
      "timeout": 120000,
      "disabled": false
    }
  }
}

▲ 중요: 위 내용에서 /path/to/your/confluence-mcp 부분은 반드시 1단계에서 생성한 실제 프로젝트 디렉토리의 전체 경로로 변경해야 합니다.


3. 실행 및 사용법

모든 설정이 완료되었습니다. 아래 명령어를 통해 서버를 테스트하고 Q CLI에서 사용할 수 있습니다.

1. 의존성 설치 및 서버 직접 테스트:

# 프로젝트 디렉토리에서 아래 명령어를 실행하여
# pyproject.toml에 정의된 의존성을 설치하고 서버를 실행합니다.
uvx --from . --with-editable python mcp_atlassian.py

성공적으로 실행되면 Confluence 초기화 성공 메시지 등이 나타납니다.

2. Q CLI에서 사용하기:

# Q CLI 대화형 모드 시작
q chat

이제 Q CLI 프롬프트에서 자연어로 명령을 내릴 수 있습니다.

  • "MMC 스페이스에서 MCP 관련 페이지를 검색해주세요"
  • "새로운 테스트 페이지를 생성해주세요"
  • "기존 페이지를 업데이트해주세요"

사용 가능한 도구 목록:

도구명 설명 주요 매개변수
confluence_search 페이지 검색 query, space_key, limit
confluence_get_page 페이지 조회 page_id, expand
confluence_create_page 페이지 생성 title, content, space_key
confluence_update_page 페이지 업데이트 page_id, title, content

4. 문제 해결

설정 및 실행 과정에서 문제가 발생할 경우 아래 사항들을 확인해 보세요.

  • 인증 오류: .env 파일에 설정된 API 토큰, 이메일 주소, Confluence URL이 정확한지 다시 확인하세요.
  • 연결 오류: 방화벽 설정이나 네트워크 연결 상태를 점검하세요.
  • MCP 서버 오류: 의존성 패키지가 모두 올바르게 설치되었는지 확인하세요.
  • 상세 로그 확인: 문제의 원인을 파악하기 어려울 경우, .env 파일에 MCP_VERBOSE=true를 설정하고, Q CLI를 q chat --verbose 옵션으로 실행하면 훨씬 더 자세한 로그를 볼 수 있습니다.

결론: 개발 생산성의 새로운 지평

지금까지 Amazon Q Developer와 MCP를 활용하여 Confluence 작업을 자동화하는 방법을 알아보았습니다. 이 가이드에서 소개한 내용은 시작에 불과합니다. 이 구조를 응용하면 Confluence뿐만 아니라 Jira 티켓 관리, Github 이슈 트래킹 등 다양한 외부 도구와의 연동으로 확장할 수 있습니다.

반복적인 문서 작업을 AI 비서에게 맡기고, 개발자는 더 창의적이고 중요한 문제에 집중할 수 있는 환경. Amazon Q와 MCP가 열어주는 개발 생산성의 새로운 미래입니다. 지금 바로 여러분의 개발 워크플로우에 AI의 날개를 달아보세요.


관련 자료 (추가 리소스)

더 깊이 있는 정보가 필요하시면 아래 공식 문서들을 참고하세요.

 

 

AWS의 파트너사인 MegazoneCloud 소속으로 AWS Ambassador로 활동하며 작성한 내용입니다.
AWS의 서비스를 소개하고, 실제 업무에서 사용한 사례들에 대한 내용들을 담고 있습니다.
Written By. Karam Kim