# MCP Tool 예시 (데이터 시각화)

## 아래 코드는 MCP 도구 예시 코드 중 `수치 데이터를 시각화` 하는 도구입니다.

### 아래 코드를 GenOS 에서 `도구 > MCP 도구 > MCP 도구 상세` 메뉴 코드 부분에 입력하면 됩니다.

```python
@mcp.tool()
async def generate_chart_html(data_json) -> str:
    """
    Chart.js를 사용하여 HTML 차트를 생성하고 업로드된 URL을 반환합니다.
    
    지원하는 차트 타입:
    - 'bar', 'line', 'pie': 기본 차트 (타입 간 전환 버튼 포함)
    - 'mixed': 여러 데이터셋을 가진 혼합 차트
    - 'dual_axis': 이중 축 차트

    Args:
        data_json (str | dict): 차트 데이터 JSON 문자열 또는 딕셔너리 객체
            기본 차트 형식:
            {
                "chart_type": "bar|line|pie",
                "title": "차트 제목",
                "x_values": ["A", "B", "C"],
                "y_values": [10, 20, 30],
                "y_label": "Y축 라벨"
            }
            
            혼합 차트 형식:
            {
                "chart_type": "mixed",
                "title": "월별 매출과 이익",
                "x_values": ["1월", "2월", "3월", "4월"],
                "datasets": [
                    {"type": "bar", "label": "매출", "data": [500, 700, 800, 600]},
                    {"type": "line", "label": "이익", "data": [120, 150, 170, 160]}
                ],
                "y_label": "금액(만원)"
            }
            
            이중축 차트 형식:
            {
                "chart_type": "dual_axis",
                "title": "월별 매출 및 성장률",
                "x_values": ["1월", "2월", "3월", "4월"],
                "datasets": [
                    {"type": "bar", "label": "매출", "data": [500, 700, 800, 600], "yAxisID": "y1"},
                    {"type": "line", "label": "성장률", "data": [12, 9, 15, 11], "yAxisID": "y2"}
                ],
                "y_axes": [
                    {"id": "y1", "label": "매출(만원)"},
                    {"id": "y2", "label": "성장률(%)"}
                ]
            }

    Returns:
        str: 성공시 iframe HTML 태그, 실패시 에러 메시지
    """

    import json
    import os
    import uuid
    import requests
    from datetime import datetime

    
    HTML_TEMPLATE = """
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>{title}</title>
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
        <style>
            .button-group {{
                margin-bottom: 10px;
            }}
            .switch-btn {{
                display: inline-block;
                margin-right: 8px;
                padding: 6px 16px;
                font-size: 15px;
                border: 1px solid #bbb;
                border-radius: 2px;
                background: #f5f5f5;
                cursor: pointer;
                color: #222;
            }}
            .switch-btn.active {{
                background: #222;
                color: #fff;
            }}
        </style>
    </head>
    <body>
        <h2>{title}</h2>
        {button_html}
        <canvas id="myChart" width="700" height="400"></canvas>
        <script>
        {chart_js}
        </script>
    </body>
    </html>
    """

    def gen_unique_filename(prefix="chart", ext="html"):
        ts = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
        uid = str(uuid.uuid4())[:8]
        return f"{prefix}_{ts}_{uid}.{ext}"

    def _get_compatible_chart_js(data):
        x_values = data['x_values']
        y_values = data['y_values']
        y_label = data.get('y_label', '')
        title = data.get('title', '')
        cur_type = data['chart_type']
        js = f"""
    let chartType = '{cur_type}';
    let chart;
    const xValues = {json.dumps(x_values)};
    const yValues = {json.dumps(y_values)};
    const chartTitle = {json.dumps(title)};
    const yLabel = {json.dumps(y_label)};

    function getDataset(type) {{
        if(type === 'bar') {{
            return [{{
                label: yLabel,
                data: yValues,
                backgroundColor: 'rgba(54,162,235,0.6)'
            }}];
        }}
        if(type === 'line') {{
            return [{{
                label: yLabel,
                data: yValues,
                borderColor: 'rgba(255,99,132,0.8)',
                backgroundColor: 'rgba(255,99,132,0.35)',
                fill: false,
                tension: 0.2
            }}];
        }}
        if(type === 'pie') {{
            return [{{
                label: yLabel,
                data: yValues,
                backgroundColor: [
                    'rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)',
                    'rgba(255, 206, 86, 0.6)', 'rgba(75, 192, 192, 0.6)',
                    'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)'
                ]
            }}];
        }}
    }}

    function getConfig(type) {{
        let config = {{
            type: type,
            data: {{
                labels: xValues,
                datasets: getDataset(type),
            }},
            options: {{
                responsive: false,
                plugins: {{
                    legend: {{
                        display: type==='pie'
                    }}
                }},
                scales: (type === 'bar' || type === 'line') ? {{
                    x: {{ title: {{ display: false }} }},
                    y: {{ title: {{ display: true, text: yLabel }} }}
                }} : {{}}
            }}
        }};
        return config;
    }}

    function renderChart(type) {{
        let ctx = document.getElementById('myChart').getContext('2d');
        if(chart) chart.destroy();
        chart = new Chart(ctx, getConfig(type));
    }}

    function switchChartType(type) {{
        chartType = type;
        renderChart(type);
        // 버튼 스타일 변경
        let btns = document.querySelectorAll('.switch-btn');
        btns.forEach(btn => {{
            if(btn.textContent.toLowerCase() === type) btn.classList.add('active');
            else btn.classList.remove('active');
        }});
    }}

    window.onload = function() {{
        renderChart(chartType);
    }};
    """
        return js

    def _get_mixed_chart_js(data):
        x_values = data['x_values']
        title = data.get('title', '')
        datasets = data['datasets']
        y_label = data.get('y_label', '값')
        colors = [
            'rgba(54,162,235,0.6)',
            'rgba(255,99,132,0.6)',
            'rgba(255,206,86,0.6)',
            'rgba(75,192,192,0.6)',
            'rgba(153,102,255,0.6)'
        ]
        js_datasets = []
        for idx, ds in enumerate(datasets):
            obj = dict(ds)
            obj['backgroundColor'] = colors[idx % len(colors)]
            obj['borderColor'] = colors[idx % len(colors)]
            # line 차트의 경우 fill 설정
            if ds.get('type') == 'line':
                obj['fill'] = False
                obj['tension'] = 0.2
            if 'yAxisID' not in obj:
                obj['yAxisID'] = 'y'
            js_datasets.append(obj)
        
        js = f"""
    let xValues = {json.dumps(x_values)};
    let chartTitle = {json.dumps(title)};
    let datasets = {json.dumps(js_datasets)};
    let yLabel = {json.dumps(y_label)};
    let ctx = document.getElementById('myChart').getContext('2d');
    let chart = new Chart(ctx, {{
        type: 'bar',  // 기본 타입, 각 dataset의 type으로 오버라이드됨
        data: {{
            labels: xValues,
            datasets: datasets,
        }},
        options: {{
            responsive: false,
            plugins: {{
                legend: {{
                    display: true
                }}
            }},
            scales: {{
                x: {{ title: {{ display: false }} }},
                y: {{
                    title: {{ display: true, text: yLabel }},
                    beginAtZero: true
                }}
            }}
        }}
    }});
    """
        return js

    def _get_dual_axis_chart_js(data):
        x_values = data['x_values']
        title = data.get('title', '')
        datasets = data['datasets']
        y_axes = data['y_axes']
        colors = [
            'rgba(54,162,235,0.6)',
            'rgba(255,99,132,0.6)',
            'rgba(255,206,86,0.6)',
            'rgba(75,192,192,0.6)',
            'rgba(153,102,255,0.6)'
        ]
        js_datasets = []
        for idx, ds in enumerate(datasets):
            obj = dict(ds)
            obj['backgroundColor'] = colors[idx % len(colors)]
            obj['borderColor'] = colors[idx % len(colors)]
            # line 차트의 경우 fill 설정
            if ds.get('type') == 'line':
                obj['fill'] = False
                obj['tension'] = 0.2
            js_datasets.append(obj)
        
        js_y_axes = {}
        for y in y_axes:
            js_y_axes[y['id']] = {
                'type': 'linear',
                'position': 'left' if y['id']=='y1' else 'right',
                'title': {'display': True, 'text': y['label']},
                'beginAtZero': True,
                'grid': {'drawOnChartArea': y['id']=='y1'}
            }
        js = f"""
    let xValues = {json.dumps(x_values)};
    let chartTitle = {json.dumps(title)};
    let datasets = {json.dumps(js_datasets)};
    let ctx = document.getElementById('myChart').getContext('2d');
    let chart = new Chart(ctx, {{
        type: 'bar',  // 기본 타입, 각 dataset의 type으로 오버라이드됨
        data: {{
            labels: xValues,
            datasets: datasets,
        }},
        options: {{
            responsive: false,
            plugins: {{
                legend: {{
                    display: true
                }}
            }},
            scales: {json.dumps(js_y_axes)}
        }}
    }});
    """
        return js

    def generate_chart_html(data):
        chart_type = data['chart_type']
        title = data.get('title', '')

        group1 = ['bar', 'line', 'pie']

        if chart_type in group1:
            cur_type = chart_type
            button_html = '<div class="button-group">'
            for ct in group1:
                cls = 'switch-btn' + (' active' if ct == cur_type else '')
                button_html += f'<button class="{cls}" onclick="switchChartType(\'{ct}\')">{ct.capitalize()}</button>'
            button_html += '</div>'
            chart_js = _get_compatible_chart_js(data)
        elif chart_type == 'mixed':
            button_html = ''
            chart_js = _get_mixed_chart_js(data)
        elif chart_type == 'dual_axis':
            button_html = ''
            chart_js = _get_dual_axis_chart_js(data)
        else:
            raise ValueError(f"Unknown chart_type: {chart_type}")

        html = HTML_TEMPLATE.format(title=title, button_html=button_html, chart_js=chart_js)
        return html

    def upload_to_temp_and_get_url(file_path):
        url = "http://llmops-cdn-api-service:8080/minio/upload/temp"
        with open(file_path, "rb") as file:
            files = {"file": (os.path.basename(file_path), file, "application/octet-stream")}
            response = requests.post(
                url,
                data={
                    "hostname": os.getenv('G__CLUSTER_HOSTNAME')
                },
                files=files
            )
        response.raise_for_status()
        return response.json()['data']['presigned_url']

    try:
        # 1. JSON 파싱 및 데이터 검증 - dict와 str 둘 다 처리
        if isinstance(data_json, dict):
            data = data_json
        elif isinstance(data_json, str):
            try:
                data = json.loads(data_json)
            except json.JSONDecodeError as e:
                return f"ERROR: JSON 파싱 실패 - {str(e)}"
        else:
            return f"ERROR: 지원하지 않는 입력 타입 '{type(data_json)}'. str 또는 dict 타입이어야 합니다."
        
        # 필수 필드 검증
        if 'chart_type' not in data:
            return "ERROR: 'chart_type' 필드가 누락되었습니다."
        
        chart_type = data['chart_type']
        supported_types = ['bar', 'line', 'pie', 'mixed', 'dual_axis']
        if chart_type not in supported_types:
            return f"ERROR: 지원하지 않는 chart_type '{chart_type}'. 지원 타입: {supported_types}"
        
        # 기본 차트 타입 검증
        if chart_type in ['bar', 'line', 'pie']:
            if 'x_values' not in data or 'y_values' not in data:
                return "ERROR: 기본 차트에는 'x_values'와 'y_values' 필드가 필요합니다."
            if len(data['x_values']) != len(data['y_values']):
                return "ERROR: x_values와 y_values의 길이가 일치하지 않습니다."
        
        # 혼합/이중축 차트 검증
        elif chart_type in ['mixed', 'dual_axis']:
            if 'x_values' not in data or 'datasets' not in data:
                return "ERROR: 혼합/이중축 차트에는 'x_values'와 'datasets' 필드가 필요합니다."
            if not data['datasets']:
                return "ERROR: datasets가 비어있습니다."
            if chart_type == 'dual_axis' and 'y_axes' not in data:
                return "ERROR: 이중축 차트에는 'y_axes' 필드가 필요합니다."

        # 2. HTML 생성
        try:
            html = generate_chart_html(data)
        except Exception as e:
            return f"ERROR: HTML 생성 실패 - {str(e)}"

        # 3. 파일 저장
        try:
            save_dir = 'charts'
            os.makedirs(save_dir, exist_ok=True)
            filename = gen_unique_filename()
            file_path = os.path.join(save_dir, filename)
            
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(html)
        except Exception as e:
            return f"ERROR: 파일 저장 실패 - {str(e)}"

        # 4. 업로드 및 URL 생성
        try:
            url = upload_to_temp_and_get_url(file_path)
            # iframe 태그로 감싸서 반환
            iframe_code = f'<iframe src="{url}" style="width:100%;height:100%;min-width:800px;min-height:600px;border:none;"></iframe>'
            return iframe_code
        except requests.exceptions.RequestException as e:
            return f"ERROR: 파일 업로드 실패 - {str(e)}"
        except KeyError as e:
            return f"ERROR: 업로드 응답 형식 오류 - {str(e)}"
        except Exception as e:
            return f"ERROR: URL 생성 실패 - {str(e)}"
            
    except Exception as e:
        return f"ERROR: 예상치 못한 오류 - {str(e)}"
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://genos-docs.gitbook.io/default/v1.8.5/basic-tutorials/guides/mcp/example-mcp-draw-chart.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
