338 lines
8.6 KiB
Go
338 lines
8.6 KiB
Go
|
|
// 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
|