MCP Tool 예시 (데이터 시각화)
아래 코드는 MCP 도구 예시 코드 중 수치 데이터를 시각화
하는 도구입니다.
수치 데이터를 시각화
하는 도구입니다.아래 코드를 GenOS 에서 도구 > MCP 도구 > MCP 도구 상세
메뉴 코드 부분에 입력하면 됩니다.
도구 > MCP 도구 > MCP 도구 상세
메뉴 코드 부분에 입력하면 됩니다.@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)}"
Last updated
Was this helpful?