Files
ttychart-mcp/tools/provider.go

338 lines
8.6 KiB
Go
Raw Normal View History

// Package tools ttychart-mcp 工具实现包
//
// 该包提供所有 MCP 工具的具体实现,包括:
// - plot_line: 折线图
// - plot_bar: 柱状图
// - plot_scatter: 散点图
//
// 作者: titor
// 创建日期: 2026-04-15
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"hub.gaomia.site/titor/ttychart-mcp/tools/charts"
"charm.land/lipgloss/v2"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// NewServer 创建新的 MCP 服务器
func NewServer() *server.MCPServer {
srv := server.NewMCPServer(
"ttychart",
"0.1.0",
server.WithToolCapabilities(true),
)
// 折线图工具
plotLineTool := mcp.NewTool(
"plot_line",
mcp.WithDescription("绘制折线图,用于展示数据随时间或类别的变化趋势"),
mcp.WithNumber("data",
mcp.Required(),
mcp.Description("Y轴数据点数组"),
),
mcp.WithString("title",
mcp.Description("图表标题"),
),
mcp.WithNumber("width",
mcp.Description("图表宽度(字符数),默认自动计算"),
),
mcp.WithNumber("height",
mcp.Description("图表高度(行数),默认自动计算"),
),
mcp.WithString("labels",
mcp.Description("X轴标签多个标签用逗号分隔"),
),
mcp.WithString("color",
mcp.Description("线条颜色,可选值: red, green, blue, yellow, cyan, magenta"),
),
)
// 柱状图工具
plotBarTool := mcp.NewTool(
"plot_bar",
mcp.WithDescription("绘制柱状图,用于展示分类数据的比较"),
mcp.WithString("data",
mcp.Required(),
mcp.Description("数据,格式: 标签1:数值1,标签2:数值2"),
),
mcp.WithString("title",
mcp.Description("图表标题"),
),
mcp.WithNumber("width",
mcp.Description("图表宽度"),
),
mcp.WithNumber("height",
mcp.Description("图表高度"),
),
mcp.WithBoolean("horizontal",
mcp.Description("是否为水平柱状图,默认为垂直"),
),
mcp.WithString("color",
mcp.Description("柱体颜色"),
),
)
// 散点图工具
plotScatterTool := mcp.NewTool(
"plot_scatter",
mcp.WithDescription("绘制散点图,用于展示数据的相关性"),
mcp.WithString("data",
mcp.Required(),
mcp.Description("数据点,格式: x1,y1 x2,y2 ..."),
),
mcp.WithString("title",
mcp.Description("图表标题"),
),
mcp.WithNumber("width",
mcp.Description("图表宽度"),
),
mcp.WithNumber("height",
mcp.Description("图表高度"),
),
mcp.WithString("x_label",
mcp.Description("X轴标签"),
),
mcp.WithString("y_label",
mcp.Description("Y轴标签"),
),
mcp.WithString("color",
mcp.Description("点颜色"),
),
)
srv.AddTool(plotLineTool, handlePlotLineTool)
srv.AddTool(plotBarTool, handlePlotBarTool)
srv.AddTool(plotScatterTool, handlePlotScatterTool)
return srv
}
// handlePlotLineTool 处理折线图工具调用
func handlePlotLineTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 将请求参数转换为 JSON 再解析
jsonBytes, err := json.Marshal(request.Params)
if err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
var args struct {
Data interface{} `json:"data"`
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
Labels string `json:"labels"`
Color string `json:"color"`
}
if err := json.Unmarshal(jsonBytes, &args); err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
// 处理数据
var data []float64
switch v := args.Data.(type) {
case float64:
data = []float64{v}
case []interface{}:
data = make([]float64, 0, len(v))
for _, item := range v {
if f, ok := toFloat64(item); ok {
data = append(data, f)
}
}
}
if len(data) == 0 {
return mcp.NewToolResultError("错误: data 参数无效"), nil
}
// 解析标签
var labels []string
if args.Labels != "" {
labels = strings.Split(args.Labels, ",")
}
// 获取颜色
color := getColorStyle(args.Color)
// 生成图表
graph := charts.NewLineChart(args.Width, args.Height)
graph.SetData(data)
graph.SetTitle(args.Title)
graph.SetLabels(labels)
graph.SetColor(color)
return mcp.NewToolResultText(graph.Render()), nil
}
// handlePlotBarTool 处理柱状图工具调用
func handlePlotBarTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
jsonBytes, err := json.Marshal(request.Params)
if err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
var args struct {
Data interface{} `json:"data"`
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
Horizontal bool `json:"horizontal"`
Color string `json:"color"`
}
if err := json.Unmarshal(jsonBytes, &args); err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
dataStr, ok := args.Data.(string)
if !ok {
return mcp.NewToolResultError("错误: data 参数格式无效"), nil
}
// 解析数据: "标签1:数值1,标签2:数值2"
data := parseBarData(dataStr)
if len(data) == 0 {
return mcp.NewToolResultError("错误: data 参数格式无效"), nil
}
color := getColorStyle(args.Color)
graph := charts.NewBarChart(args.Width, args.Height, args.Horizontal)
graph.SetData(data)
graph.SetTitle(args.Title)
graph.SetColor(color)
return mcp.NewToolResultText(graph.Render()), nil
}
// handlePlotScatterTool 处理散点图工具调用
func handlePlotScatterTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
jsonBytes, err := json.Marshal(request.Params)
if err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
var args struct {
Data interface{} `json:"data"`
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
XLabel string `json:"x_label"`
YLabel string `json:"y_label"`
Color string `json:"color"`
}
if err := json.Unmarshal(jsonBytes, &args); err != nil {
return mcp.NewToolResultError("参数解析失败"), err
}
dataStr, ok := args.Data.(string)
if !ok {
return mcp.NewToolResultError("错误: data 参数格式无效"), nil
}
// 解析数据: "x1,y1 x2,y2 ..."
data := parseScatterData(dataStr)
if len(data) == 0 {
return mcp.NewToolResultError("错误: data 参数格式无效"), nil
}
color := getColorStyle(args.Color)
graph := charts.NewScatterChart(args.Width, args.Height)
graph.SetData(data)
graph.SetTitle(args.Title)
graph.SetXLabel(args.XLabel)
graph.SetYLabel(args.YLabel)
graph.SetColor(color)
return mcp.NewToolResultText(graph.Render()), nil
}
// parseBarData 解析柱状图数据
// 格式: "标签1:数值1,标签2:数值2"
func parseBarData(str string) []charts.BarDataItem {
var result []charts.BarDataItem
items := strings.Split(str, ",")
for _, item := range items {
parts := strings.SplitN(item, ":", 2)
if len(parts) == 2 {
if value, err := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64); err == nil {
result = append(result, charts.BarDataItem{
Label: strings.TrimSpace(parts[0]),
Value: value,
})
}
}
}
return result
}
// parseScatterData 解析散点图数据
// 格式: "x1,y1 x2,y2 ..."
func parseScatterData(str string) []charts.ScatterDataItem {
var result []charts.ScatterDataItem
items := strings.Fields(str)
for _, item := range items {
parts := strings.SplitN(item, ",", 2)
if len(parts) == 2 {
x, err1 := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
y, err2 := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
if err1 == nil && err2 == nil {
result = append(result, charts.ScatterDataItem{
X: x,
Y: y,
})
}
}
}
return result
}
// getColorStyle 获取颜色样式
func getColorStyle(colorName string) lipgloss.Style {
switch colorName {
case "red":
return lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
case "green":
return lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
case "blue":
return lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
case "yellow":
return lipgloss.NewStyle().Foreground(lipgloss.Color("11"))
case "cyan":
return lipgloss.NewStyle().Foreground(lipgloss.Color("14"))
case "magenta":
return lipgloss.NewStyle().Foreground(lipgloss.Color("13"))
default:
return lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
}
}
// toFloat64 将 interface{} 转换为 float64
func toFloat64(v interface{}) (float64, bool) {
switch n := v.(type) {
case float64:
return n, true
case int:
return float64(n), true
case int64:
return float64(n), true
default:
return 0, false
}
}
// 这里添加 fmt 的使用以避免导入错误
var _ = fmt.Sprintf