From ec61960c1e8f4e69533672b4e86f3679b1ccac8d Mon Sep 17 00:00:00 2001 From: OpenWaygate <27287694+OpenWaygate@users.noreply.github.com> Date: Sat, 27 Sep 2025 00:23:34 +0800 Subject: [PATCH] :boom: Migrate to official mcp sdk --- MODULE.bazel | 3 +- cmd/BUILD.bazel | 2 +- cmd/activity/BUILD.bazel | 3 +- cmd/activity/list.go | 179 +++++----- cmd/caption/BUILD.bazel | 3 +- cmd/caption/delete.go | 93 ++--- cmd/caption/download.go | 130 ++++--- cmd/caption/insert.go | 246 ++++++++------ cmd/caption/list.go | 150 ++++---- cmd/caption/update.go | 244 +++++++------ cmd/channel/BUILD.bazel | 3 +- cmd/channel/list.go | 219 ++++++------ cmd/channel/update.go | 152 +++++---- cmd/channelBanner/BUILD.bazel | 3 +- cmd/channelBanner/insert.go | 117 ++++--- cmd/channelSection/BUILD.bazel | 3 +- cmd/channelSection/delete.go | 80 +++-- cmd/channelSection/list.go | 156 +++++---- cmd/comment/BUILD.bazel | 3 +- cmd/comment/delete.go | 67 ++-- cmd/comment/insert.go | 140 ++++---- cmd/comment/list.go | 147 ++++---- cmd/comment/markAsSpam.go | 89 ++--- cmd/comment/setModerationStatus.go | 116 ++++--- cmd/comment/update.go | 126 ++++--- cmd/commentThread/BUILD.bazel | 3 +- cmd/commentThread/insert.go | 117 ++++--- cmd/commentThread/list.go | 204 ++++++----- cmd/i18nLanguage/BUILD.bazel | 2 +- cmd/i18nLanguage/list.go | 83 ++--- cmd/i18nRegion/BUILD.bazel | 2 +- cmd/i18nRegion/list.go | 42 +-- cmd/mcp.go | 45 +-- cmd/member/BUILD.bazel | 3 +- cmd/member/list.go | 139 ++++---- cmd/membershipsLevel/BUILD.bazel | 3 +- cmd/membershipsLevel/list.go | 93 ++--- cmd/playlist/BUILD.bazel | 3 +- cmd/playlist/delete.go | 80 +++-- cmd/playlist/insert.go | 147 ++++---- cmd/playlist/list.go | 184 +++++----- cmd/playlist/update.go | 155 +++++---- cmd/playlistImage/BUILD.bazel | 3 +- cmd/playlistImage/delete.go | 80 +++-- cmd/playlistImage/insert.go | 131 ++++--- cmd/playlistImage/list.go | 139 ++++---- cmd/playlistImage/update.go | 120 ++++--- cmd/playlistItem/BUILD.bazel | 3 +- cmd/playlistItem/delete.go | 80 +++-- cmd/playlistItem/insert.go | 184 +++++----- cmd/playlistItem/list.go | 157 +++++---- cmd/playlistItem/update.go | 136 ++++---- cmd/search/BUILD.bazel | 3 +- cmd/search/list.go | 452 ++++++++++++++----------- cmd/subscription/BUILD.bazel | 3 +- cmd/subscription/delete.go | 67 ++-- cmd/subscription/insert.go | 117 ++++--- cmd/subscription/list.go | 223 ++++++------ cmd/superChatEvent/BUILD.bazel | 3 +- cmd/superChatEvent/list.go | 115 ++++--- cmd/thumbnail/BUILD.bazel | 3 +- cmd/thumbnail/set.go | 94 ++--- cmd/video/BUILD.bazel | 3 +- cmd/video/delete.go | 67 ++-- cmd/video/getRating.go | 102 +++--- cmd/video/insert.go | 311 +++++++++-------- cmd/video/list.go | 228 +++++++------ cmd/video/rate.go | 78 +++-- cmd/video/reportAbuse.go | 125 ++++--- cmd/video/update.go | 212 ++++++------ cmd/videoAbuseReportReason/BUILD.bazel | 3 +- cmd/videoAbuseReportReason/list.go | 108 +++--- cmd/videoCategory/BUILD.bazel | 2 +- cmd/videoCategory/list.go | 43 +-- cmd/watermark/BUILD.bazel | 3 +- cmd/watermark/set.go | 143 ++++---- cmd/watermark/unset.go | 63 ++-- go.mod | 35 +- go.sum | 76 ++--- pkg/utils/BUILD.bazel | 1 + pkg/utils/utils.go | 8 + 81 files changed, 4226 insertions(+), 3277 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 8387e9d..fffab90 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,8 +17,9 @@ go_deps.config( ) use_repo( go_deps, + "com_github_google_jsonschema_go", "com_github_jedib0t_go_pretty_v6", - "com_github_mark3labs_mcp_go", + "com_github_modelcontextprotocol_go_sdk", "com_github_ohler55_ojg", "com_github_savioxavier_termlink", "com_github_spf13_cobra", diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index ef1e3e0..a04b8f8 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -20,7 +20,7 @@ go_library( deps = [ "//pkg", "//pkg/auth", - "@com_github_mark3labs_mcp_go//server", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_savioxavier_termlink//:termlink", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_viper//:viper", diff --git a/cmd/activity/BUILD.bazel b/cmd/activity/BUILD.bazel index d5cfc9d..9520e9d 100644 --- a/cmd/activity/BUILD.bazel +++ b/cmd/activity/BUILD.bazel @@ -13,7 +13,8 @@ go_library( "//pkg", "//pkg/activity", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/activity/list.go b/cmd/activity/list.go index 8560d3d..39e8035 100644 --- a/cmd/activity/list.go +++ b/cmd/activity/list.go @@ -3,6 +3,7 @@ package activity import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,14 +11,90 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/activity" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) -var defaultParts = []string{"id", "snippet", "contentDetails"} +type listIn struct { + ChannelId string `json:"channelId"` + Home *string `json:"home,omitempty"` + MaxResults int64 `json:"maxResults"` + Mine *string `json:"mine,omitempty"` + PublishedAfter string `json:"publishedAfter"` + PublishedBefore string `json:"publishedBefore"` + RegionCode string `json:"regionCode"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "channelId", "home", "maxResults", "mine", "publishedAfter", + "publishedBefore", "regionCode", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "channelId": { + Type: "string", Description: ciUsage, + Default: json.RawMessage(`""`), + }, + "home": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: homeUsage, Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "mine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mineUsage, Default: json.RawMessage(`""`), + }, + "publishedAfter": { + Type: "string", Description: paUsage, + Default: json.RawMessage(`""`), + }, + "publishedBefore": { + Type: "string", Description: pbUsage, + Default: json.RawMessage(`""`), + }, + "regionCode": { + Type: "string", Description: rcUsage, + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet","contentDetails"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "activity-list", Title: short, Description: long, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) activityCmd.AddCommand(listCmd) listCmd.Flags().StringVarP(&channelId, "channelId", "c", "", ciUsage) listCmd.Flags().BoolVarP(home, "home", "H", true, homeUsage) @@ -31,7 +108,8 @@ func init() { ) listCmd.Flags().StringVarP(®ionCode, "regionCode", "r", "", rcUsage) listCmd.Flags().StringSliceVarP( - &parts, "parts", "p", defaultParts, pkg.PartsUsage, + &parts, "parts", "p", []string{"id", "snippet", "contentDetails"}, + pkg.PartsUsage, ) listCmd.Flags().StringVarP(&output, "output", "o", "table", pkg.TableUsage) listCmd.Flags().StringVarP(&jpath, "jsonpath", "j", "", pkg.JPUsage) @@ -50,80 +128,19 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "activity-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(ciUsage), mcp.Required(), - ), - mcp.WithString( - "home", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(homeUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "mine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mineUsage), mcp.Required(), - ), - mcp.WithString( - "publishedAfter", mcp.DefaultString(""), - mcp.Description(paUsage), mcp.Required(), - ), - mcp.WithString( - "publishedBefore", mcp.DefaultString(""), - mcp.Description(pbUsage), mcp.Required(), - ), - mcp.WithString( - "regionCode", mcp.DefaultString(""), - mcp.Description(rcUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray(defaultParts), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - channelId, _ = args["channelId"].(string) - homeRaw, _ := args["home"].(string) - home = utils.BoolPtr(homeRaw) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - mineRaw, ok := args["mine"].(string) - if !ok { - mineRaw = "true" // Default to true if not provided - } - mine = utils.BoolPtr(mineRaw) - publishedAfter, _ = args["publishedAfter"].(string) - publishedBefore, _ = args["publishedBefore"].(string) - regionCode, _ = args["regionCode"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + channelId = input.ChannelId + home = utils.BoolPtr(*input.Home) + maxResults = input.MaxResults + mine = utils.BoolPtr(*input.Mine) + publishedAfter = input.PublishedAfter + publishedBefore = input.PublishedBefore + regionCode = input.RegionCode + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "activity list started") @@ -131,17 +148,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "activity list failed", - "error", err, - "args", args, + ctx, "activity list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "activity list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/caption/BUILD.bazel b/cmd/caption/BUILD.bazel index 8752ba3..659f6e6 100644 --- a/cmd/caption/BUILD.bazel +++ b/cmd/caption/BUILD.bazel @@ -17,7 +17,8 @@ go_library( "//pkg", "//pkg/caption", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_pflag//:pflag", ], diff --git a/cmd/caption/delete.go b/cmd/caption/delete.go index 248f31e..9f58f78 100644 --- a/cmd/caption/delete.go +++ b/cmd/caption/delete.go @@ -3,12 +3,14 @@ package caption import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/caption" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,50 @@ const ( deleteIdsUsage = "IDs of the captions to delete" ) +type deleteIn struct { + Ids []string `json:"ids"` + OnBehalfOf string `json:"onBehalfOf"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOf", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOf": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "caption-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) captionCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -44,39 +88,12 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "caption-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOf", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOf, _ = args["onBehalfOf"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOf = input.OnBehalfOf + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "caption delete started") @@ -84,17 +101,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "caption delete failed", - "error", err, - "args", args, + ctx, "caption delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "caption delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/caption/download.go b/cmd/caption/download.go index 305af26..003ceff 100644 --- a/cmd/caption/download.go +++ b/cmd/caption/download.go @@ -3,12 +3,14 @@ package caption import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/caption" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,69 @@ const ( downloadIdUsage = "ID of the caption to download" ) +type downloadIn struct { + Ids []string `json:"ids"` + File string `json:"file"` + Tfmt string `json:"tfmt"` + Tlang string `json:"tlang"` + OnBehalfOf string `json:"onBehalfOf"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var downloadInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "file", "tfmt", "tlang", "onBehalfOf", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: downloadIdUsage, + Default: json.RawMessage(`[]`), + }, + "file": { + Type: "string", + Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "tfmt": { + Type: "string", + Enum: []any{"sbv", "srt", "vtt", ""}, + Description: tfmtUsage, + Default: json.RawMessage(`""`), + }, + "tlang": { + Type: "string", + Description: tlangUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOf": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(downloadTool, downloadHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "caption-download", Title: downloadShort, Description: downloadLong, + InputSchema: downloadInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, downloadHandler, + ) captionCmd.AddCommand(downloadCmd) downloadCmd.Flags().StringSliceVarP( @@ -50,54 +113,15 @@ var downloadCmd = &cobra.Command{ }, } -var downloadTool = mcp.NewTool( - "caption-download", - mcp.WithTitleAnnotation(downloadShort), - mcp.WithDescription(downloadLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(downloadIdUsage), mcp.Required(), - ), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "tfmt", mcp.Enum("sbv", "srt", "vtt"), - mcp.DefaultString(""), mcp.Description(tfmtUsage), mcp.Required(), - ), - mcp.WithString( - "tlang", mcp.DefaultString(""), - mcp.Description(tlangUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOf", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func downloadHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - file, _ = args["file"].(string) - tfmt, _ = args["tfmt"].(string) - tlang, _ = args["tlang"].(string) - onBehalfOf, _ = args["onBehalfOf"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input downloadIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + file = input.File + tfmt = input.Tfmt + tlang = input.Tlang + onBehalfOf = input.OnBehalfOf + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "caption download started") @@ -105,17 +129,15 @@ func downloadHandler( err := download(&writer) if err != nil { slog.ErrorContext( - ctx, "caption download failed", - "error", err, - "args", args, + ctx, "caption download failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "caption download completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func download(writer io.Writer) error { diff --git a/cmd/caption/insert.go b/cmd/caption/insert.go index 65c343e..1f96d3a 100644 --- a/cmd/caption/insert.go +++ b/cmd/caption/insert.go @@ -3,6 +3,7 @@ package caption import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/caption" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,130 @@ const ( insertLong = "Insert caption to a video" ) +type insertIn struct { + File string `json:"file"` + AudioTrackType string `json:"audioTrackType"` + IsAutoSynced *string `json:"isAutoSynced,omitempty"` + IsCC *string `json:"isCC,omitempty"` + IsDraft *string `json:"isDraft,omitempty"` + IsEasyReader *string `json:"isEasyReader,omitempty"` + IsLarge *string `json:"isLarge,omitempty"` + Language string `json:"language"` + Name string `json:"name"` + TrackKind string `json:"trackKind"` + VideoId string `json:"videoId"` + OnBehalfOf string `json:"onBehalfOf"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "file", "audioTrackType", "isAutoSynced", "isCC", "isDraft", + "isEasyReader", "isLarge", "language", "name", "trackKind", + "videoId", "onBehalfOf", "onBehalfOfContentOwner", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "file": { + Type: "string", + Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "audioTrackType": { + Type: "string", + Enum: []any{"unknown", "primary", "commentary", "descriptive"}, + Description: attUsage, + Default: json.RawMessage(`"unknown"`), + }, + "isAutoSynced": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iasUsage, + Default: json.RawMessage(`""`), + }, + "isCC": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iscUsage, + Default: json.RawMessage(`""`), + }, + "isDraft": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: isdUsage, + Default: json.RawMessage(`""`), + }, + "isEasyReader": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iserUsage, + Default: json.RawMessage(`""`), + }, + "isLarge": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: islUsage, + Default: json.RawMessage(`""`), + }, + "language": { + Type: "string", + Description: languageUsage, + Default: json.RawMessage(`""`), + }, + "name": { + Type: "string", + Description: nameUsage, + Default: json.RawMessage(`""`), + }, + "trackKind": { + Type: "string", + Enum: []any{"standard", "ASR", "forced"}, + Description: tkUsage, + Default: json.RawMessage(`"standard"`), + }, + "videoId": { + Type: "string", + Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOf": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", + Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, + Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", + Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "caption-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) captionCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP(&file, "file", "f", "", fileUsage) @@ -66,100 +190,24 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "caption-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "audioTrackType", - mcp.Enum("unknown", "primary", "commentary", "descriptive"), - mcp.DefaultString("unknown"), mcp.Description(attUsage), mcp.Required(), - ), - mcp.WithString( - "isAutoSynced", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iasUsage), mcp.Required(), - ), - mcp.WithString( - "isCC", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iscUsage), mcp.Required(), - ), - mcp.WithString( - "isDraft", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(isdUsage), mcp.Required(), - ), - mcp.WithString( - "isEasyReader", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iserUsage), mcp.Required(), - ), - mcp.WithString( - "isLarge", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(islUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(languageUsage), mcp.Required(), - ), - mcp.WithString( - "name", mcp.DefaultString(""), - mcp.Description(nameUsage), mcp.Required(), - ), - mcp.WithString( - "trackKind", mcp.Enum("standard", "ASR", "forced"), - mcp.DefaultString("standard"), mcp.Description(tkUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOf", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - file, _ = args["file"].(string) - audioTrackType, _ = args["audioTrackType"].(string) - isAutoSyncedRaw, _ := args["isAutoSynced"].(string) - isAutoSynced = utils.BoolPtr(isAutoSyncedRaw) - isCCRaw, _ := args["isCC"].(string) - isCC = utils.BoolPtr(isCCRaw) - isDraftRaw, _ := args["isDraft"].(string) - isDraft = utils.BoolPtr(isDraftRaw) - isEasyReaderRaw, _ := args["isEasyReader"].(string) - isEasyReader = utils.BoolPtr(isEasyReaderRaw) - isLargeRaw, _ := args["isLarge"].(string) - isLarge = utils.BoolPtr(isLargeRaw) - language, _ = args["language"].(string) - name, _ = args["name"].(string) - trackKind, _ = args["trackKind"].(string) - videoId, _ = args["videoId"].(string) - onBehalfOf, _ = args["onBehalfOf"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + file = input.File + audioTrackType = input.AudioTrackType + isAutoSynced = utils.BoolPtr(*input.IsAutoSynced) + isCC = utils.BoolPtr(*input.IsCC) + isDraft = utils.BoolPtr(*input.IsDraft) + isEasyReader = utils.BoolPtr(*input.IsEasyReader) + isLarge = utils.BoolPtr(*input.IsLarge) + language = input.Language + name = input.Name + trackKind = input.TrackKind + videoId = input.VideoId + onBehalfOf = input.OnBehalfOf + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "caption insert started") @@ -167,17 +215,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "caption insert failed", - "error", err, - "args", args, + ctx, "caption insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "caption insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/caption/list.go b/cmd/caption/list.go index 2b70ae8..abb23ac 100644 --- a/cmd/caption/list.go +++ b/cmd/caption/list.go @@ -3,13 +3,15 @@ package caption import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/caption" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,78 @@ const ( listIdsUsage = "IDs of the captions to list" ) +type listIn struct { + Ids []string `json:"ids"` + VideoId string `json:"videoId"` + OnBehalfOf string `json:"onBehalfOf"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "videoId", "onBehalfOf", "onBehalfOfContentOwner", + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "videoId": { + Type: "string", + Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOf": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", + Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, + Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", + Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "caption-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) captionCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -49,64 +121,16 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "caption-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOf", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - videoId, _ = args["videoId"].(string) - onBehalfOf, _ = args["onBehalfOf"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + videoId = input.VideoId + onBehalfOf = input.OnBehalfOf + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "caption list started") @@ -114,17 +138,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "caption list failed", - "error", err, - "args", args, + ctx, "caption list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "caption list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/caption/update.go b/cmd/caption/update.go index 8c9becd..07c75eb 100644 --- a/cmd/caption/update.go +++ b/cmd/caption/update.go @@ -3,6 +3,7 @@ package caption import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/caption" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,129 @@ const ( updateLong = "Update caption of a video" ) +type updateIn struct { + File string `json:"file"` + AudioTrackType string `json:"audioTrackType"` + IsAutoSynced *string `json:"isAutoSynced,omitempty"` + IsCC *string `json:"isCC,omitempty"` + IsDraft *string `json:"isDraft,omitempty"` + IsEasyReader *string `json:"isEasyReader,omitempty"` + IsLarge *string `json:"isLarge,omitempty"` + Language string `json:"language"` + Name string `json:"name"` + TrackKind string `json:"trackKind"` + VideoId string `json:"videoId"` + OnBehalfOf string `json:"onBehalfOf"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "file", "audioTrackType", "isAutoSynced", "isCC", "isDraft", + "isEasyReader", "isLarge", "language", "name", "trackKind", + "videoId", "onBehalfOf", "onBehalfOfContentOwner", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "file": { + Type: "string", + Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "audioTrackType": { + Type: "string", + Description: attUsage, + Default: json.RawMessage(`"unknown"`), + }, + "isAutoSynced": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iasUsage, + Default: json.RawMessage(`""`), + }, + "isCC": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iscUsage, + Default: json.RawMessage(`""`), + }, + "isDraft": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: isdUsage, + Default: json.RawMessage(`""`), + }, + "isEasyReader": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: iserUsage, + Default: json.RawMessage(`""`), + }, + "isLarge": { + Type: "string", + Enum: []any{"true", "false", ""}, + Description: islUsage, + Default: json.RawMessage(`""`), + }, + "language": { + Type: "string", + Description: languageUsage, + Default: json.RawMessage(`""`), + }, + "name": { + Type: "string", + Description: nameUsage, + Default: json.RawMessage(`""`), + }, + "trackKind": { + Type: "string", + Enum: []any{"standard", "ASR", "forced"}, + Description: tkUsage, + Default: json.RawMessage(`"standard"`), + }, + "videoId": { + Type: "string", + Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOf": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", + Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, + Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", + Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "caption-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) captionCmd.AddCommand(updateCmd) updateCmd.Flags().StringVarP(&file, "file", "f", "", fileUsage) @@ -65,99 +188,24 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "caption-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "audioTrackType", mcp.DefaultString("unknown"), - mcp.Description(attUsage), mcp.Required(), - ), - mcp.WithString( - "isAutoSynced", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iasUsage), mcp.Required(), - ), - mcp.WithString( - "isCC", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iscUsage), mcp.Required(), - ), - mcp.WithString( - "isDraft", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(isdUsage), mcp.Required(), - ), - mcp.WithString( - "isEasyReader", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(iserUsage), mcp.Required(), - ), - mcp.WithString( - "isLarge", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(islUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(languageUsage), mcp.Required(), - ), - mcp.WithString( - "name", mcp.DefaultString(""), - mcp.Description(nameUsage), mcp.Required(), - ), - mcp.WithString( - "trackKind", mcp.Enum("standard", "ASR", "forced"), - mcp.DefaultString("standard"), mcp.Description(tkUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOf", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - file, _ = args["file"].(string) - audioTrackType, _ = args["audioTrackType"].(string) - isAutoSyncedRaw, _ := args["isAutoSynced"].(string) - isAutoSynced = utils.BoolPtr(isAutoSyncedRaw) - isCCRaw, _ := args["isCC"].(string) - isCC = utils.BoolPtr(isCCRaw) - isDraftRaw, _ := args["isDraft"].(string) - isDraft = utils.BoolPtr(isDraftRaw) - isEasyReaderRaw, _ := args["isEasyReader"].(string) - isEasyReader = utils.BoolPtr(isEasyReaderRaw) - isLargeRaw, _ := args["isLarge"].(string) - isLarge = utils.BoolPtr(isLargeRaw) - language, _ = args["language"].(string) - name, _ = args["name"].(string) - trackKind, _ = args["trackKind"].(string) - videoId, _ = args["videoId"].(string) - onBehalfOf, _ = args["onBehalfOf"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + file = input.File + audioTrackType = input.AudioTrackType + isAutoSynced = utils.BoolPtr(*input.IsAutoSynced) + isCC = utils.BoolPtr(*input.IsCC) + isDraft = utils.BoolPtr(*input.IsDraft) + isEasyReader = utils.BoolPtr(*input.IsEasyReader) + isLarge = utils.BoolPtr(*input.IsLarge) + language = input.Language + name = input.Name + trackKind = input.TrackKind + videoId = input.VideoId + onBehalfOf = input.OnBehalfOf + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "caption update started") @@ -165,17 +213,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "caption update failed", - "error", err, - "args", args, + ctx, "caption update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "caption update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/channel/BUILD.bazel b/cmd/channel/BUILD.bazel index 030aaa7..14da353 100644 --- a/cmd/channel/BUILD.bazel +++ b/cmd/channel/BUILD.bazel @@ -14,7 +14,8 @@ go_library( "//pkg", "//pkg/channel", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_pflag//:pflag", ], diff --git a/cmd/channel/list.go b/cmd/channel/list.go index 2a4a96e..384cf62 100644 --- a/cmd/channel/list.go +++ b/cmd/channel/list.go @@ -3,6 +3,7 @@ package channel import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/channel" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -20,8 +22,104 @@ const ( listIdsUsage = "Return the channels with the specified IDs" ) +type listIn struct { + CategoryId string `json:"categoryId"` + ForHandle string `json:"forHandle"` + ForUsername string `json:"forUsername"` + Hl string `json:"hl"` + Ids []string `json:"ids"` + ManagedByMe *string `json:"managedByMe,omitempty"` + MaxResults int64 `json:"maxResults"` + Mine *string `json:"mine,omitempty"` + MySubscribers *string `json:"mySubscribers,omitempty"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "categoryId", "forHandle", "forUsername", "hl", "ids", + "managedByMe", "maxResults", "mine", "mySubscribers", + "onBehalfOfContentOwner", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "categoryId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "forHandle": { + Type: "string", Description: fhUsage, + Default: json.RawMessage(`""`), + }, + "forUsername": { + Type: "string", Description: fuUsage, + Default: json.RawMessage(`""`), + }, + "hl": { + Type: "string", Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "managedByMe": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mbmUsage, Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "mine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mineUsage, Default: json.RawMessage(`""`), + }, + "mySubscribers": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: msUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet","status"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "channel-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) channelCmd.AddCommand(listCmd) listCmd.Flags().StringVarP( @@ -68,101 +166,22 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "channel-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "categoryId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "forHandle", mcp.DefaultString(""), - mcp.Description(fhUsage), mcp.Required(), - ), - mcp.WithString( - "forUsername", mcp.DefaultString(""), - mcp.Description(fuUsage), mcp.Required(), - ), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "managedByMe", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mbmUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "mine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mineUsage), mcp.Required(), - ), - mcp.WithString( - "mySubscribers", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(msUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet", "status"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - categoryId, _ = args["categoryId"].(string) - forHandle, _ = args["forHandle"].(string) - forUsername, _ = args["forUsername"].(string) - hl, _ = args["hl"].(string) - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - managedByMeRaw, _ := args["managedByMe"].(string) - managedByMe = utils.BoolPtr(managedByMeRaw) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - mineRaw, ok := args["mine"].(string) - if !ok { - mineRaw = "true" - } - mine = utils.BoolPtr(mineRaw) - mySubscribersRaw, _ := args["mySubscribers"].(string) - mySubscribers = utils.BoolPtr(mySubscribersRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + categoryId = input.CategoryId + forHandle = input.ForHandle + forUsername = input.ForUsername + hl = input.Hl + ids = input.Ids + managedByMe = utils.BoolPtr(*input.ManagedByMe) + maxResults = input.MaxResults + mine = utils.BoolPtr(*input.Mine) + mySubscribers = utils.BoolPtr(*input.MySubscribers) + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "channel list started") @@ -170,17 +189,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "channel list failed", - "error", err, - "args", args, + ctx, "channel list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "channel list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/channel/update.go b/cmd/channel/update.go index b2571eb..9e0848d 100644 --- a/cmd/channel/update.go +++ b/cmd/channel/update.go @@ -3,13 +3,15 @@ package channel import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/channel" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,74 @@ const ( updateIdUsage = "ID of the channel to update" ) +type updateIn struct { + Ids []string `json:"ids"` + Country string `json:"country"` + CustomUrl string `json:"customUrl"` + DefaultLanguage string `json:"defaultLanguage"` + Description string `json:"description"` + Title string `json:"title"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "country", "customUrl", "defaultLanguage", + "description", "title", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: updateIdUsage, + Default: json.RawMessage(`[]`), + }, + "country": { + Type: "string", Description: countryUsage, + Default: json.RawMessage(`""`), + }, + "customUrl": { + Type: "string", Description: curlUsage, + Default: json.RawMessage(`""`), + }, + "defaultLanguage": { + Type: "string", Description: dlUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "channel-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) channelCmd.AddCommand(updateCmd) updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage) @@ -52,69 +120,17 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "channel-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(updateIdUsage), mcp.Required(), - ), - mcp.WithString( - "country", mcp.DefaultString(""), - mcp.Description(countryUsage), - mcp.Required(), - ), - mcp.WithString( - "customUrl", mcp.DefaultString(""), - mcp.Description(curlUsage), - mcp.Required(), - ), - mcp.WithString( - "defaultLanguage", mcp.DefaultString(""), - mcp.Description(dlUsage), - mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), - mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), - mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - country, _ = args["country"].(string) - customUrl, _ = args["customUrl"].(string) - defaultLanguage, _ = args["defaultLanguage"].(string) - description, _ = args["description"].(string) - title, _ = args["title"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + country = input.Country + customUrl = input.CustomUrl + defaultLanguage = input.DefaultLanguage + description = input.Description + title = input.Title + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "channel update started") @@ -122,17 +138,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "channel update failed", - "error", err, - "args", args, + ctx, "channel update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "channel update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/channelBanner/BUILD.bazel b/cmd/channelBanner/BUILD.bazel index 87fe532..d8c0183 100644 --- a/cmd/channelBanner/BUILD.bazel +++ b/cmd/channelBanner/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/channelBanner", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/channelBanner/insert.go b/cmd/channelBanner/insert.go index 14bb1f2..8373d10 100644 --- a/cmd/channelBanner/insert.go +++ b/cmd/channelBanner/insert.go @@ -3,18 +3,73 @@ package channelBanner import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/channelBanner" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type insertIn struct { + ChannelId string `json:"channelId"` + File string `json:"file"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + OnBehalfOfContentOwnerChannel string `json:"onBehalfOfContentOwnerChannel"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "channelId", "file", "onBehalfOfContentOwner", + "onBehalfOfContentOwnerChannel", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "file": { + Type: "string", Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwnerChannel": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "channelBanner-insert", Title: short, Description: long, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) channelBannerCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP(&channelId, "channelId", "c", "", cidUsage) @@ -45,49 +100,15 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "channelBanner-insert", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwnerChannel", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - channelId, _ = args["channelId"].(string) - file, _ = args["file"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - onBehalfOfContentOwnerChannel, _ = args["onBehalfOfContentOwnerChannel"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + channelId = input.ChannelId + file = input.File + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + onBehalfOfContentOwnerChannel = input.OnBehalfOfContentOwnerChannel + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "channelBanner insert started") @@ -95,17 +116,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "channelBanner insert failed", - "error", err, - "args", args, + ctx, "channelBanner insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "channelBanner insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/channelSection/BUILD.bazel b/cmd/channelSection/BUILD.bazel index 6014f23..2734712 100644 --- a/cmd/channelSection/BUILD.bazel +++ b/cmd/channelSection/BUILD.bazel @@ -14,7 +14,8 @@ go_library( "//pkg", "//pkg/channelSection", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/channelSection/delete.go b/cmd/channelSection/delete.go index b698a3a..2aa546b 100644 --- a/cmd/channelSection/delete.go +++ b/cmd/channelSection/delete.go @@ -3,12 +3,14 @@ package channelSection import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/channelSection" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,43 @@ const ( deleteIdsUsage = "Delete the channel sections with the given ids" ) +type deleteIn struct { + Ids []string `json:"ids"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "channelSection-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) channelSectionCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -43,34 +80,11 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "channelSection-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "channelSection delete started") @@ -78,17 +92,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "channelSection delete failed", - "error", err, - "args", args, + ctx, "channelSection delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "channelSection delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/channelSection/list.go b/cmd/channelSection/list.go index 0754b8b..7384131 100644 --- a/cmd/channelSection/list.go +++ b/cmd/channelSection/list.go @@ -3,6 +3,7 @@ package channelSection import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/channelSection" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -20,8 +22,77 @@ const ( listIdsUsage = "Return the channel sections with the given ids" ) +type listIn struct { + Ids []string `json:"ids"` + ChannelId string `json:"channelId"` + Hl string `json:"hl"` + Mine *string `json:"mine,omitempty"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "channelId", "hl", "mine", "onBehalfOfContentOwner", + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "hl": { + Type: "string", Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "mine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mineUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "channelSection-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) channelSectionCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -51,70 +122,17 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "channelSection-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithString( - "mine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mineUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - channelId, _ = args["channelId"].(string) - hl, _ = args["hl"].(string) - mineRaw, _ := args["mine"].(string) - mine = utils.BoolPtr(mineRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + channelId = input.ChannelId + hl = input.Hl + mine = utils.BoolPtr(*input.Mine) + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "channelSection list started") @@ -122,17 +140,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "channelSection list failed", - "error", err, - "args", args, + ctx, "channelSection list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "channelSection list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/comment/BUILD.bazel b/cmd/comment/BUILD.bazel index eee0bd9..bb3d34f 100644 --- a/cmd/comment/BUILD.bazel +++ b/cmd/comment/BUILD.bazel @@ -18,7 +18,8 @@ go_library( "//pkg", "//pkg/comment", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/comment/delete.go b/cmd/comment/delete.go index a8390ca..f3852c8 100644 --- a/cmd/comment/delete.go +++ b/cmd/comment/delete.go @@ -3,12 +3,14 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/comment" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -17,8 +19,36 @@ const ( deleteLong = "Delete YouTube comments by ids" ) +type deleteIn struct { + IDs []string `json:"ids"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"ids"}, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) commentCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) @@ -38,29 +68,10 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "comment-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.IDs slog.InfoContext(ctx, "comment delete started") @@ -68,17 +79,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "comment delete failed", - "error", err, - "args", args, + ctx, "comment delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/comment/insert.go b/cmd/comment/insert.go index acee59a..78ab207 100644 --- a/cmd/comment/insert.go +++ b/cmd/comment/insert.go @@ -3,6 +3,7 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/comment" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -20,8 +22,71 @@ const ( insertPidUsage = "ID of the parent comment" ) +type insertIn struct { + AuthorChannelId string `json:"authorChannelId"` + ChannelId string `json:"channelId"` + CanRate string `json:"canRate"` + ParentId string `json:"parentId"` + TextOriginal string `json:"textOriginal"` + VideoId string `json:"videoId"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "authorChannelId", "channelId", "canRate", "parentId", + "textOriginal", "videoId", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "authorChannelId": { + Type: "string", Description: acidUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "canRate": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: crUsage, Default: json.RawMessage(`""`), + }, + "parentId": { + Type: "string", Description: insertPidUsage, + Default: json.RawMessage(`""`), + }, + "textOriginal": { + Type: "string", Description: toUsage, + Default: json.RawMessage(`""`), + }, + "videoId": { + Type: "string", Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) commentCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP( @@ -61,60 +126,17 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "comment-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "authorChannelId", mcp.DefaultString(""), - mcp.Description(acidUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "canRate", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(crUsage), mcp.Required(), - ), - mcp.WithString( - "parentId", mcp.DefaultString(""), - mcp.Description(insertPidUsage), mcp.Required(), - ), - mcp.WithString( - "textOriginal", mcp.DefaultString(""), - mcp.Description(toUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - authorChannelId, _ = args["authorChannelId"].(string) - channelId, _ = args["channelId"].(string) - canRateRaw, _ := args["canRate"].(string) - canRate = utils.BoolPtr(canRateRaw) - parentId, _ = args["parentId"].(string) - textOriginal, _ = args["textOriginal"].(string) - videoId, _ = args["videoId"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + authorChannelId = input.AuthorChannelId + channelId = input.ChannelId + canRate = utils.BoolPtr(input.CanRate) + parentId = input.ParentId + textOriginal = input.TextOriginal + videoId = input.VideoId + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "comment insert started") @@ -122,17 +144,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "comment insert failed", - "error", err, - "args", args, + ctx, "comment insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/comment/list.go b/cmd/comment/list.go index 62df31e..1efeca8 100644 --- a/cmd/comment/list.go +++ b/cmd/comment/list.go @@ -3,13 +3,15 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/comment" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,73 @@ const ( listPidUsage = "Returns replies to the specified comment" ) +type listIn struct { + IDs []string `json:"ids"` + MaxResults int64 `json:"maxResults"` + ParentId string `json:"parentId"` + TextFormat string `json:"textFormat"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "maxResults", "parentId", "textFormat", + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "parentId": { + Type: "string", Description: listPidUsage, + Default: json.RawMessage(`""`), + }, + "textFormat": { + Type: "string", Enum: []any{"textFormatUnspecified", "html", "plainText"}, + Description: tfUsage, Default: json.RawMessage(`"html"`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) commentCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) @@ -51,66 +118,16 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "comment-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "parentId", mcp.DefaultString(""), - mcp.Description(listPidUsage), - mcp.Required(), - ), - mcp.WithString( - "textFormat", mcp.Enum("textFormatUnspecified", "html", "plainText"), - mcp.DefaultString("html"), mcp.Description(tfUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - parentId, _ = args["parentId"].(string) - textFormat, _ = args["textFormat"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.IDs + maxResults = input.MaxResults + parentId = input.ParentId + textFormat = input.TextFormat + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "comment list started") @@ -118,17 +135,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "comment list failed", - "error", err, - "args", args, + ctx, "comment list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/comment/markAsSpam.go b/cmd/comment/markAsSpam.go index 1e32cc7..ebf3bf6 100644 --- a/cmd/comment/markAsSpam.go +++ b/cmd/comment/markAsSpam.go @@ -3,13 +3,15 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/comment" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,46 @@ const ( masLong = "Mark YouTube comments as spam by ids" ) +type markAsSpamIn struct { + IDs []string `json:"ids"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var markAsSpamInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"ids", "output", "jsonpath"}, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(markAsSpamTool, markAsSpamHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-markAsSpam", Title: masShort, Description: masLong, + InputSchema: markAsSpamInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, markAsSpamHandler, + ) commentCmd.AddCommand(markAsSpamCmd) markAsSpamCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) @@ -42,39 +82,12 @@ var markAsSpamCmd = &cobra.Command{ }, } -var markAsSpamTool = mcp.NewTool( - "comment-markAsSpam", - mcp.WithTitleAnnotation(masShort), - mcp.WithDescription(masLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func markAsSpamHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input markAsSpamIn, +) (*mcp.CallToolResult, any, error) { + ids = input.IDs + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "comment markAsSpam started") @@ -82,17 +95,15 @@ func markAsSpamHandler( err := markAsSpam(&writer) if err != nil { slog.ErrorContext( - ctx, "comment markAsSpam failed", - "error", err, - "args", args, + ctx, "comment markAsSpam failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment markAsSpam completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func markAsSpam(writer io.Writer) error { diff --git a/cmd/comment/setModerationStatus.go b/cmd/comment/setModerationStatus.go index 4c21a4b..b143c06 100644 --- a/cmd/comment/setModerationStatus.go +++ b/cmd/comment/setModerationStatus.go @@ -3,6 +3,7 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/comment" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,60 @@ const ( smsLong = "Set YouTube comments moderation status by ids" ) +type setModerationStatusIn struct { + IDs []string `json:"ids"` + ModerationStatus string `json:"moderationStatus"` + BanAuthor string `json:"banAuthor"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var setModerationStatusInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "moderationStatus", "banAuthor", + "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + "moderationStatus": { + Type: "string", Enum: []any{"heldForReview", "published", "rejected", ""}, + Description: msUsage, Default: json.RawMessage(`""`), + }, + "banAuthor": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: baUsage, Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(setModerationStatusTool, setModerationStatusHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-setModerationStatus", Title: smsShort, Description: smsLong, + InputSchema: setModerationStatusInSchema, + Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, setModerationStatusHandler, + ) commentCmd.AddCommand(setModerationStatusCmd) setModerationStatusCmd.Flags().StringSliceVarP( @@ -56,50 +110,14 @@ var setModerationStatusCmd = &cobra.Command{ }, } -var setModerationStatusTool = mcp.NewTool( - "comment-setModerationStatus", - mcp.WithTitleAnnotation(smsShort), - mcp.WithDescription(smsLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), - mcp.WithString( - "moderationStatus", mcp.Enum("heldForReview", "published", "rejected"), - mcp.DefaultString(""), mcp.Description(msUsage), mcp.Required(), - ), - mcp.WithString( - "banAuthor", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(baUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func setModerationStatusHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - moderationStatus, _ = args["moderationStatus"].(string) - banAuthorRaw, _ := args["banAuthor"].(string) - banAuthor = utils.BoolPtr(banAuthorRaw) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input setModerationStatusIn, +) (*mcp.CallToolResult, any, error) { + ids = input.IDs + moderationStatus = input.ModerationStatus + banAuthor = utils.BoolPtr(input.BanAuthor) + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "comment setModerationStatus started") @@ -107,17 +125,15 @@ func setModerationStatusHandler( err := setModerationStatus(&writer) if err != nil { slog.ErrorContext( - ctx, "comment setModerationStatus failed", - "error", err, - "args", args, + ctx, "comment setModerationStatus failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment setModerationStatus completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func setModerationStatus(writer io.Writer) error { diff --git a/cmd/comment/update.go b/cmd/comment/update.go index afd7776..8700fa2 100644 --- a/cmd/comment/update.go +++ b/cmd/comment/update.go @@ -3,6 +3,7 @@ package comment import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/comment" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -20,8 +22,64 @@ const ( updateIdUsage = "ID of the comment" ) +type updateIn struct { + IDs []string `json:"ids"` + CanRate string `json:"canRate"` + TextOriginal string `json:"textOriginal"` + ViewerRating string `json:"viewerRating"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "canRate", "textOriginal", "viewerRating", + "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: updateIdUsage, + Default: json.RawMessage(`[]`), + }, + "canRate": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: crUsage, Default: json.RawMessage(`""`), + }, + "textOriginal": { + Type: "string", Description: toUsage, + Default: json.RawMessage(`""`), + }, + "viewerRating": { + Type: "string", Enum: []any{"none", "like", "dislike", ""}, + Description: vrUsage, Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "comment-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) commentCmd.AddCommand(updateCmd) updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage) @@ -51,55 +109,15 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "comment-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(updateIdUsage), mcp.Required(), - ), - mcp.WithString( - "canRate", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(crUsage), mcp.Required(), - ), - mcp.WithString( - "textOriginal", mcp.DefaultString(""), - mcp.Description(toUsage), mcp.Required(), - ), - mcp.WithString( - "viewerRating", mcp.Enum("none", "like", "dislike"), - mcp.DefaultString(""), mcp.Description(vrUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - canRateRaw, _ := args["canRate"].(string) - canRate = utils.BoolPtr(canRateRaw) - textOriginal, _ = args["textOriginal"].(string) - viewerRating, _ = args["viewerRating"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.IDs + canRate = utils.BoolPtr(input.CanRate) + textOriginal = input.TextOriginal + viewerRating = input.ViewerRating + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "comment update started") @@ -107,17 +125,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "comment update failed", - "error", err, - "args", args, + ctx, "comment update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "comment update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/commentThread/BUILD.bazel b/cmd/commentThread/BUILD.bazel index f7fab66..0148fdf 100644 --- a/cmd/commentThread/BUILD.bazel +++ b/cmd/commentThread/BUILD.bazel @@ -13,7 +13,8 @@ go_library( "//cmd", "//pkg", "//pkg/commentThread", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/commentThread/insert.go b/cmd/commentThread/insert.go index 50eb5a9..6625f65 100644 --- a/cmd/commentThread/insert.go +++ b/cmd/commentThread/insert.go @@ -3,13 +3,15 @@ package commentThread import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/commentThread" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,61 @@ const ( insertVidUsage = "ID of the video" ) +type insertIn struct { + AuthorChannelId string `json:"authorChannelId"` + ChannelId string `json:"channelId"` + TextOriginal string `json:"textOriginal"` + VideoId string `json:"videoId"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "authorChannelId", "channelId", "textOriginal", "videoId", + "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "authorChannelId": { + Type: "string", Description: acidUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "textOriginal": { + Type: "string", Description: toUsage, + Default: json.RawMessage(`""`), + }, + "videoId": { + Type: "string", Description: insertVidUsage, + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "commentThread-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) commentThreadCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP( &authorChannelId, "authorChannelId", "a", "", acidUsage, @@ -50,49 +105,15 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "commentThread-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "authorChannelId", mcp.DefaultString(""), - mcp.Description(acidUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "textOriginal", mcp.DefaultString(""), - mcp.Description(toUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(insertVidUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - authorChannelId, _ = args["authorChannelId"].(string) - channelId, _ = args["channelId"].(string) - textOriginal, _ = args["textOriginal"].(string) - videoId, _ = args["videoId"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + authorChannelId = input.AuthorChannelId + channelId = input.ChannelId + textOriginal = input.TextOriginal + videoId = input.VideoId + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "commentThread insert started") @@ -100,17 +121,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "commentThread insert failed", - "error", err, - "args", args, + ctx, "commentThread insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "commentThread insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/commentThread/list.go b/cmd/commentThread/list.go index ad89b19..e7bad2e 100644 --- a/cmd/commentThread/list.go +++ b/cmd/commentThread/list.go @@ -3,13 +3,15 @@ package commentThread import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/commentThread" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,100 @@ const ( listVidUsage = "Returns the comment threads of the specified video" ) +type listIn struct { + Ids []string `json:"ids"` + AllThreadsRelatedToChannelId string `json:"allThreadsRelatedToChannelId"` + ChannelId string `json:"channelId"` + MaxResults int64 `json:"maxResults"` + ModerationStatus string `json:"moderationStatus"` + Order string `json:"order"` + SearchTerms string `json:"searchTerms"` + TextFormat string `json:"textFormat"` + VideoId string `json:"videoId"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "allThreadsRelatedToChannelId", "channelId", "maxResults", + "moderationStatus", "order", "searchTerms", "textFormat", + "videoId", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + "allThreadsRelatedToChannelId": { + Type: "string", Description: atrtcidUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "moderationStatus": { + Type: "string", + Enum: []any{"published", "heldForReview", "likelySpam", "rejected"}, + Description: msUsage, Default: json.RawMessage(`"published"`), + }, + "order": { + Type: "string", Enum: []any{"orderUnspecified", "time", "relevance"}, + Description: orderUsage, Default: json.RawMessage(`"time"`), + }, + "searchTerms": { + Type: "string", Description: stUsage, + Default: json.RawMessage(`""`), + }, + "textFormat": { + Type: "string", Enum: []any{"textFormatUnspecified", "html"}, + Description: tfUsage, Default: json.RawMessage(`"html"`), + }, + "videoId": { + Type: "string", Description: listVidUsage, + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "commentThread-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) commentThreadCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) @@ -57,91 +151,21 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "commentThread-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), - mcp.WithString( - "allThreadsRelatedToChannelId", mcp.DefaultString(""), - mcp.Description(atrtcidUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "moderationStatus", - mcp.Enum("published", "heldForReview", "likelySpam", "rejected"), - mcp.DefaultString("published"), mcp.Description(msUsage), mcp.Required(), - ), - mcp.WithString( - "order", mcp.Enum("orderUnspecified", "time", "relevance"), - mcp.DefaultString("time"), mcp.Description(orderUsage), mcp.Required(), - ), - mcp.WithString( - "searchTerms", mcp.DefaultString(""), - mcp.Description(stUsage), mcp.Required(), - ), - mcp.WithString( - "textFormat", mcp.Enum("textFormatUnspecified", "html"), - mcp.DefaultString("html"), mcp.Description(tfUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(listVidUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - allThreadsRelatedToChannelId, _ = args["allThreadsRelatedToChannelId"].(string) - channelId, _ = args["channelId"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - moderationStatus, _ = args["moderationStatus"].(string) - order, _ = args["order"].(string) - searchTerms, _ = args["searchTerms"].(string) - textFormat, _ = args["textFormat"].(string) - videoId, _ = args["videoId"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + allThreadsRelatedToChannelId = input.AllThreadsRelatedToChannelId + channelId = input.ChannelId + maxResults = input.MaxResults + moderationStatus = input.ModerationStatus + order = input.Order + searchTerms = input.SearchTerms + textFormat = input.TextFormat + videoId = input.VideoId + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "commentThread list started") @@ -149,17 +173,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "commentThread list failed", - "error", err, - "args", args, + ctx, "commentThread list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "commentThread list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/i18nLanguage/BUILD.bazel b/cmd/i18nLanguage/BUILD.bazel index d6dd9b1..285b6ea 100644 --- a/cmd/i18nLanguage/BUILD.bazel +++ b/cmd/i18nLanguage/BUILD.bazel @@ -13,7 +13,7 @@ go_library( "//pkg", "//pkg/i18nLanguage", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/i18nLanguage/list.go b/cmd/i18nLanguage/list.go index ccc9c9d..04d578c 100644 --- a/cmd/i18nLanguage/list.go +++ b/cmd/i18nLanguage/list.go @@ -10,13 +10,33 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/i18nLanguage" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) func init() { - cmd.MCP.AddResource(hlResource, hlHandler) - cmd.MCP.AddResourceTemplate(langsResource, langsHandler) + cmd.Server.AddResource( + &mcp.Resource{ + URI: hlURI, + Name: hlName, + Description: hlDesc, + MIMEType: pkg.JsonMIME, + Annotations: &mcp.Annotations{ + Audience: []mcp.Role{"user", "assistant"}, Priority: 0.51, + }, + }, hlHandler, + ) + cmd.Server.AddResourceTemplate( + &mcp.ResourceTemplate{ + Name: langName, + Description: long, + MIMEType: pkg.JsonMIME, + URITemplate: langURI, + Annotations: &mcp.Annotations{ + Audience: []mcp.Role{"user", "assistant"}, Priority: 0.51, + }, + }, langsHandler, + ) i18nLanguageCmd.AddCommand(listCmd) listCmd.Flags().StringVarP(&hl, "hl", "l", "", hlUsage) listCmd.Flags().StringSliceVarP( @@ -39,16 +59,9 @@ var listCmd = &cobra.Command{ }, } -var hlResource = mcp.NewResource( - hlURI, hlName, - mcp.WithMIMEType(pkg.JsonMIME), - mcp.WithResourceDescription(hlDesc), - mcp.WithAnnotations([]mcp.Role{"user", "assistant"}, 0.51), -) - func hlHandler( - ctx context.Context, request mcp.ReadResourceRequest, -) ([]mcp.ResourceContents, error) { + ctx context.Context, request *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { parts = defaultParts output = "json" jpath = "$.*.snippet.hl" @@ -59,9 +72,8 @@ func hlHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "i18nLanguage hl list failed", - "error", err, - "uri", request.Params.URI, + ctx, "i18nLanguage hl list failed", "error", err, "uri", + request.Params.URI, ) return nil, err } @@ -71,26 +83,18 @@ func hlHandler( "resultSize", writer.Len(), ) - contents := []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: hlURI, - MIMEType: pkg.JsonMIME, - Text: writer.String(), + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: hlURI, MIMEType: pkg.JsonMIME, Text: writer.String(), + }, }, - } - return contents, nil + }, nil } -var langsResource = mcp.NewResourceTemplate( - langURI, langName, - mcp.WithTemplateMIMEType(pkg.JsonMIME), - mcp.WithTemplateDescription(long), - mcp.WithTemplateAnnotations([]mcp.Role{"user", "assistant"}, 0.51), -) - func langsHandler( - ctx context.Context, request mcp.ReadResourceRequest, -) ([]mcp.ResourceContents, error) { + ctx context.Context, request *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { parts = defaultParts hl = utils.ExtractHl(request.Params.URI) output = "json" @@ -101,9 +105,7 @@ func langsHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "i18nLanguage list failed", - "error", err, - "uri", request.Params.URI, + ctx, "i18nLanguage list failed", "error", err, "uri", request.Params.URI, ) return nil, err } @@ -113,14 +115,13 @@ func langsHandler( "resultSize", writer.Len(), ) - contents := []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: request.Params.URI, - MIMEType: pkg.JsonMIME, - Text: writer.String(), + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: request.Params.URI, MIMEType: pkg.JsonMIME, Text: writer.String(), + }, }, - } - return contents, nil + }, nil } func list(writer io.Writer) error { diff --git a/cmd/i18nRegion/BUILD.bazel b/cmd/i18nRegion/BUILD.bazel index d50de7d..f3aca7b 100644 --- a/cmd/i18nRegion/BUILD.bazel +++ b/cmd/i18nRegion/BUILD.bazel @@ -13,7 +13,7 @@ go_library( "//pkg", "//pkg/i18nRegion", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/i18nRegion/list.go b/cmd/i18nRegion/list.go index d932759..e221690 100644 --- a/cmd/i18nRegion/list.go +++ b/cmd/i18nRegion/list.go @@ -10,12 +10,22 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/i18nRegion" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) func init() { - cmd.MCP.AddResourceTemplate(RegionsResource, regionsHandler) + cmd.Server.AddResourceTemplate( + &mcp.ResourceTemplate{ + Name: regionName, + Description: long, + MIMEType: pkg.JsonMIME, + URITemplate: regionURI, + Annotations: &mcp.Annotations{ + Audience: []mcp.Role{"user", "assistant"}, Priority: 0.51, + }, + }, regionsHandler, + ) i18nRegionCmd.AddCommand(listCmd) listCmd.Flags().StringVarP(&hl, "hl", "l", "", hlUsage) listCmd.Flags().StringSliceVarP( @@ -38,16 +48,9 @@ var listCmd = &cobra.Command{ }, } -var RegionsResource = mcp.NewResourceTemplate( - regionURI, regionName, - mcp.WithTemplateMIMEType(pkg.JsonMIME), - mcp.WithTemplateDescription(long), - mcp.WithTemplateAnnotations([]mcp.Role{"user", "assistant"}, 0.51), -) - func regionsHandler( - ctx context.Context, request mcp.ReadResourceRequest, -) ([]mcp.ResourceContents, error) { + ctx context.Context, request *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { parts = defaultParts hl = utils.ExtractHl(request.Params.URI) output = "json" @@ -58,9 +61,7 @@ func regionsHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "i18nRegion list failed", - "error", err, - "uri", request.Params.URI, + ctx, "i18nRegion list failed", "error", err, "uri", request.Params.URI, ) return nil, err } @@ -70,14 +71,13 @@ func regionsHandler( "resultSize", writer.Len(), ) - contents := []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: request.Params.URI, - MIMEType: pkg.JsonMIME, - Text: writer.String(), + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: request.Params.URI, MIMEType: pkg.JsonMIME, Text: writer.String(), + }, }, - } - return contents, nil + }, nil } func list(writer io.Writer) error { diff --git a/cmd/mcp.go b/cmd/mcp.go index aeca717..4839e2c 100644 --- a/cmd/mcp.go +++ b/cmd/mcp.go @@ -4,10 +4,11 @@ import ( "context" "fmt" "log/slog" + "net/http" "os" "time" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -23,12 +24,14 @@ var ( port int ) -var MCP = server.NewMCPServer( - "yutu", Version, - server.WithToolCapabilities(true), - server.WithResourceCapabilities(true, true), - server.WithLogging(), - server.WithRecovery(), +var Server = mcp.NewServer( + &mcp.Implementation{Name: "yutu", Version: Version}, + &mcp.ServerOptions{ + PageSize: 99, + KeepAlive: 13 * time.Second, + HasResources: true, + HasTools: true, + }, ) var mcpCmd = &cobra.Command{ @@ -36,9 +39,8 @@ var mcpCmd = &cobra.Command{ Short: mcpShort, Long: mcpLong, Run: func(cmd *cobra.Command, args []string) { - ctx := context.Background() var err error - interval := 13 * time.Second + ctx := context.Background() addr := fmt.Sprintf(":%d", port) slog.InfoContext( ctx, "starting MCP server", @@ -48,33 +50,32 @@ var mcpCmd = &cobra.Command{ switch mode { case "stdio": - err = server.ServeStdio(MCP) + t := &mcp.LoggingTransport{ + Transport: &mcp.StdioTransport{}, + Writer: os.Stderr, + } + err = Server.Run(ctx, t) case "http": - httpServer := server.NewStreamableHTTPServer( - MCP, - server.WithHeartbeatInterval(interval), + handler := mcp.NewStreamableHTTPHandler( + func(*http.Request) *mcp.Server { + return Server + }, nil, ) - slog.InfoContext( ctx, "http server configuration", "url", fmt.Sprintf("http://localhost:%d/mcp", port), - "heartbeat_interval", interval, ) - err = httpServer.Start(addr) + err = http.ListenAndServe(addr, handler) default: slog.ErrorContext( - ctx, "invalid server mode", - "mode", mode, - "valid_modes", []string{"stdio", "http"}, + ctx, "invalid mode", "mode", mode, "valid_modes", "stdio, http", ) os.Exit(1) } if err != nil { slog.ErrorContext( - ctx, "starting server failed", - "error", err, - "mode", mode, + ctx, "starting server failed", "error", err, "mode", mode, ) os.Exit(1) } diff --git a/cmd/member/BUILD.bazel b/cmd/member/BUILD.bazel index 94674db..759264d 100644 --- a/cmd/member/BUILD.bazel +++ b/cmd/member/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/member", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/member/list.go b/cmd/member/list.go index 925eed7..5b755d0 100644 --- a/cmd/member/list.go +++ b/cmd/member/list.go @@ -3,18 +3,83 @@ package member import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/member" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + MemberChannelId string `json:"memberChannelId"` + HasAccessToLevel string `json:"hasAccessToLevel"` + MaxResults int64 `json:"maxResults"` + Mode string `json:"mode"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "memberChannelId", "hasAccessToLevel", "maxResults", "mode", + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "memberChannelId": { + Type: "string", Description: mcidUsage, + Default: json.RawMessage(`""`), + }, + "hasAccessToLevel": { + Type: "string", Description: hatlUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "mode": { + Type: "string", + Enum: []any{"listMembersModeUnknown", "updates", "all_current"}, + Description: mmUsage, Default: json.RawMessage(`"all_current"`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "member-list", Title: short, Description: long, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) memberCmd.AddCommand(listCmd) listCmd.Flags().StringVarP( @@ -45,60 +110,16 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "member-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "memberChannelId", mcp.DefaultString(""), - mcp.Description(mcidUsage), mcp.Required(), - ), - mcp.WithString( - "hasAccessToLevel", mcp.DefaultString(""), - mcp.Description(hatlUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "mode", mcp.Enum("listMembersModeUnknown", "updates", "all_current"), - mcp.DefaultString("all_current"), mcp.Description(mmUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - memberChannelId, _ = args["memberChannelId"].(string) - hasAccessToLevel, _ = args["hasAccessToLevel"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - mode, _ = args["mode"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + memberChannelId = input.MemberChannelId + hasAccessToLevel = input.HasAccessToLevel + maxResults = input.MaxResults + mode = input.Mode + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "member list started") @@ -106,17 +127,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "member list failed", - "error", err, - "args", args, + ctx, "member list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "member list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/membershipsLevel/BUILD.bazel b/cmd/membershipsLevel/BUILD.bazel index d703473..e66ba82 100644 --- a/cmd/membershipsLevel/BUILD.bazel +++ b/cmd/membershipsLevel/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/membershipsLevel", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/membershipsLevel/list.go b/cmd/membershipsLevel/list.go index a51d264..9167b31 100644 --- a/cmd/membershipsLevel/list.go +++ b/cmd/membershipsLevel/list.go @@ -3,22 +3,64 @@ package membershipsLevel import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/membershipsLevel" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id", "snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "membershipsLevel-list", Title: short, Description: long, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) membershipsLevelCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP( - &parts, "parts", "p", []string{"id, snippet"}, pkg.PartsUsage, + &parts, "parts", "p", []string{"id", "snippet"}, pkg.PartsUsage, ) listCmd.Flags().StringVarP(&output, "output", "o", "table", pkg.TableUsage) listCmd.Flags().StringVarP(&jpath, "jsonpath", "j", "", pkg.JPUsage) @@ -37,39 +79,12 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "membershipsLevel-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id, snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "membershipsLevel list started") @@ -77,17 +92,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "membershipsLevel list failed", - "error", err, - "args", args, + ctx, "membershipsLevel list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "membershipsLevel list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/playlist/BUILD.bazel b/cmd/playlist/BUILD.bazel index 9f62d2c..6dd1b9c 100644 --- a/cmd/playlist/BUILD.bazel +++ b/cmd/playlist/BUILD.bazel @@ -16,7 +16,8 @@ go_library( "//pkg", "//pkg/playlist", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/playlist/delete.go b/cmd/playlist/delete.go index 3e242cf..cd1cbc0 100644 --- a/cmd/playlist/delete.go +++ b/cmd/playlist/delete.go @@ -3,12 +3,14 @@ package playlist import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/playlist" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,43 @@ const ( deleteIdsUsage = "IDs of the playlists to delete" ) +type deleteIn struct { + Ids []string `json:"ids"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlist-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) playlistCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -43,34 +80,11 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "playlist-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "playlist delete started") @@ -78,17 +92,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "playlist delete failed", - "error", err, - "args", args, + ctx, "playlist delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlist delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/playlist/insert.go b/cmd/playlist/insert.go index 63b216f..5c1ffac 100644 --- a/cmd/playlist/insert.go +++ b/cmd/playlist/insert.go @@ -3,13 +3,15 @@ package playlist import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlist" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,74 @@ const ( insertCidUsage = "Channel id of the playlist" ) +type insertIn struct { + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags"` + Language string `json:"language"` + ChannelId string `json:"channelId"` + Privacy string `json:"privacy"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "title", "description", "tags", "language", + "channelId", "privacy", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "tags": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: tagsUsage, + Default: json.RawMessage(`[]`), + }, + "language": { + Type: "string", Description: languageUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: insertCidUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlist-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) playlistCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP(&title, "title", "t", "", titleUsage) @@ -50,64 +118,17 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "playlist-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithArray( - "tags", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(tagsUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(languageUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(insertCidUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - title, _ = args["title"].(string) - description, _ = args["description"].(string) - tagsRaw, _ := args["tags"].([]any) - tags = make([]string, len(tagsRaw)) - for i, tag := range tagsRaw { - tags[i] = tag.(string) - } - language, _ = args["language"].(string) - channelId, _ = args["channelId"].(string) - privacy, _ = args["privacy"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + title = input.Title + description = input.Description + tags = input.Tags + language = input.Language + channelId = input.ChannelId + privacy = input.Privacy + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlist insert started") @@ -115,17 +136,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "playlist insert failed", - "error", err, - "args", args, + ctx, "playlist insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlist insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/playlist/list.go b/cmd/playlist/list.go index dfc8741..0cb744e 100644 --- a/cmd/playlist/list.go +++ b/cmd/playlist/list.go @@ -3,6 +3,7 @@ package playlist import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlist" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -21,8 +23,89 @@ const ( listCidUsage = "Return the playlists owned by the specified channel id" ) +type listIn struct { + Ids []string `json:"ids"` + ChannelId string `json:"channelId"` + Hl string `json:"hl"` + MaxResults int64 `json:"maxResults"` + Mine *string `json:"mine,omitempty"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + OnBehalfOfContentOwnerChannel string `json:"onBehalfOfContentOwnerChannel"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "channelId", "hl", "maxResults", "mine", + "onBehalfOfContentOwner", "onBehalfOfContentOwnerChannel", + "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "channelId": { + Type: "string", Description: listCidUsage, + Default: json.RawMessage(`""`), + }, + "hl": { + Type: "string", Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "mine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mineUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwnerChannel": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet","status"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlist-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) playlistCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -56,84 +139,19 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "playlist-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(listCidUsage), mcp.Required(), - ), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "mine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mineUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwnerChannel", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet", "status"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids := make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - channelId, _ = args["channelId"].(string) - hl, _ = args["hl"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - mineRaw, ok := args["mine"].(string) - if !ok { - mineRaw = "true" - } - mine = utils.BoolPtr(mineRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - onBehalfOfContentOwnerChannel, _ = args["onBehalfOfContentOwnerChannel"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + channelId = input.ChannelId + hl = input.Hl + maxResults = input.MaxResults + mine = utils.BoolPtr(*input.Mine) + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + onBehalfOfContentOwnerChannel = input.OnBehalfOfContentOwnerChannel + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlist list started") @@ -141,17 +159,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "playlist list failed", - "error", err, - "args", args, + ctx, "playlist list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlist list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/playlist/update.go b/cmd/playlist/update.go index 97759ee..a7c3eb6 100644 --- a/cmd/playlist/update.go +++ b/cmd/playlist/update.go @@ -3,13 +3,15 @@ package playlist import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlist" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,77 @@ const ( updateIdUsage = "ID of the playlist to update" ) +type updateIn struct { + Ids []string `json:"ids"` + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags"` + Language string `json:"language"` + Privacy string `json:"privacy"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "title", "description", "tags", + "language", "privacy", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: updateIdUsage, + Default: json.RawMessage(`[]`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "tags": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: tagsUsage, + Default: json.RawMessage(`[]`), + }, + "language": { + Type: "string", Description: languageUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlist-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) playlistCmd.AddCommand(updateCmd) updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage) @@ -48,69 +119,17 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "playlist-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(updateIdUsage), mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithArray( - "tags", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(tagsUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(languageUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - title, _ = args["title"].(string) - description, _ = args["description"].(string) - tagsRaw, _ := args["tags"].([]any) - tags = make([]string, len(tagsRaw)) - for i, tag := range tagsRaw { - tags[i] = tag.(string) - } - language, _ = args["language"].(string) - privacy, _ = args["privacy"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + title = input.Title + description = input.Description + tags = input.Tags + language = input.Language + privacy = input.Privacy + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlist update started") @@ -118,17 +137,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "playlist update failed", - "error", err, - "args", args, + ctx, "playlist update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlist update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/playlistImage/BUILD.bazel b/cmd/playlistImage/BUILD.bazel index 8b29fb3..842b5bc 100644 --- a/cmd/playlistImage/BUILD.bazel +++ b/cmd/playlistImage/BUILD.bazel @@ -15,7 +15,8 @@ go_library( "//cmd", "//pkg", "//pkg/playlistImage", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/playlistImage/delete.go b/cmd/playlistImage/delete.go index 0bba10d..8a4d64d 100644 --- a/cmd/playlistImage/delete.go +++ b/cmd/playlistImage/delete.go @@ -3,22 +3,59 @@ package playlistImage import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/playlistImage" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type deleteIn struct { + Ids []string `json:"ids"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: idsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + const ( deleteShort = "Delete YouTube playlist images" deleteLong = "Delete YouTube playlist images by ids" ) func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistImage-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) playlistImageCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) @@ -42,34 +79,11 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "playlistImage-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(idsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "playlistImage delete started") @@ -77,17 +91,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistImage delete failed", - "error", err, - "args", args, + ctx, "playlistImage delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistImage delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/playlistImage/insert.go b/cmd/playlistImage/insert.go index 2407051..76c444b 100644 --- a/cmd/playlistImage/insert.go +++ b/cmd/playlistImage/insert.go @@ -3,23 +3,84 @@ package playlistImage import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistImage" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type insertIn struct { + File string `json:"file"` + PlaylistId string `json:"playlistId"` + Type string `json:"type"` + Height int64 `json:"height"` + Width int64 `json:"width"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "file", "playlistId", "type", "height", "width", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "file": { + Type: "string", Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "playlistId": { + Type: "string", Description: pidUsage, + Default: json.RawMessage(`""`), + }, + "type": { + Type: "string", Description: typeUsage, + Default: json.RawMessage(`""`), + }, + "height": { + Type: "number", Description: heightUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "width": { + Type: "number", Description: widthUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( insertShort = "Insert a YouTube playlist image" insertLong = "Insert a YouTube playlist image for a given playlist id" ) func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistImage-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) playlistImageCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP(&file, "file", "f", "", fileUsage) @@ -47,56 +108,16 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "playlistImage-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(pidUsage), mcp.Required(), - ), - mcp.WithString( - "type", mcp.DefaultString(""), - mcp.Description(typeUsage), mcp.Required(), - ), - mcp.WithNumber( - "height", mcp.DefaultNumber(0), - mcp.Description(heightUsage), mcp.Required(), - ), - mcp.WithNumber( - "width", mcp.DefaultNumber(0), - mcp.Description(widthUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - file, _ = args["file"].(string) - playlistId, _ = args["playlistId"].(string) - type_, _ = args["type"].(string) - heightRaw, _ := args["height"].(float64) - height = int64(heightRaw) - widthRaw, _ := args["width"].(float64) - width = int64(widthRaw) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + file = input.File + playlistId = input.PlaylistId + type_ = input.Type + height = input.Height + width = input.Width + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlistImage insert started") @@ -104,17 +125,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistImage insert failed", - "error", err, - "args", args, + ctx, "playlistImage insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistImage insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/playlistImage/list.go b/cmd/playlistImage/list.go index bc6d0dd..ee27789 100644 --- a/cmd/playlistImage/list.go +++ b/cmd/playlistImage/list.go @@ -3,25 +3,88 @@ package playlistImage import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistImage" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + Parent string `json:"parent"` + MaxResults int64 `json:"maxResults"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + OnBehalfOfContentOwnerChannel string `json:"onBehalfOfContentOwnerChannel"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "parent", "maxResults", "parts", "output", "jsonpath", + "onBehalfOfContentOwner", "onBehalfOfContentOwnerChannel", + }, + Properties: map[string]*jsonschema.Schema{ + "parent": { + Type: "string", Description: parentUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","kind","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwnerChannel": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + const ( listShort = "List YouTube playlist images" listLong = "List YouTube playlist images' info" ) func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistImage-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) playlistImageCmd.AddCommand(listCmd) - listCmd.Flags().StringVarP(&parent, "parent", "P", "", parentUsage) listCmd.Flags().Int64VarP(&maxResults, "maxResults", "n", 5, pkg.MRUsage) listCmd.Flags().StringSliceVarP( @@ -50,60 +113,16 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "playlistImage-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "parent", mcp.DefaultString(""), - mcp.Description(parentUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "kind", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwnerChannel", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - parent, _ = args["parent"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - onBehalfOfContentOwnerChannel, _ = args["onBehalfOfContentOwnerChannel"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + parent = input.Parent + maxResults = input.MaxResults + parts = input.Parts + output = input.Output + jpath = input.Jsonpath + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + onBehalfOfContentOwnerChannel = input.OnBehalfOfContentOwnerChannel slog.InfoContext(ctx, "playlistImage list started") @@ -111,17 +130,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistImage list failed", - "error", err, - "args", args, + ctx, "playlistImage list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistImage list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/playlistImage/update.go b/cmd/playlistImage/update.go index 04dfc7d..c412ce0 100644 --- a/cmd/playlistImage/update.go +++ b/cmd/playlistImage/update.go @@ -3,23 +3,79 @@ package playlistImage import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistImage" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type updateIn struct { + PlaylistId string `json:"playlistId"` + Type string `json:"type"` + Height int64 `json:"height"` + Width int64 `json:"width"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "playlistId", "type", "height", "width", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "playlistId": { + Type: "string", Description: pidUsage, + Default: json.RawMessage(`""`), + }, + "type": { + Type: "string", Description: typeUsage, + Default: json.RawMessage(`""`), + }, + "height": { + Type: "number", Description: heightUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "width": { + Type: "number", Description: widthUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( updateShort = "Update a playlist image" updateLong = "Update a playlist image for a given playlist id" ) func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistImage-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) playlistImageCmd.AddCommand(updateCmd) updateCmd.Flags().StringVarP(&playlistId, "playlistId", "p", "", pidUsage) @@ -45,51 +101,15 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "playlistImage-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(pidUsage), mcp.Required(), - ), - mcp.WithString( - "type", mcp.DefaultString(""), - mcp.Description(typeUsage), mcp.Required(), - ), - mcp.WithNumber( - "height", mcp.DefaultNumber(0), - mcp.Description(heightUsage), mcp.Required(), - ), - mcp.WithNumber( - "width", mcp.DefaultNumber(0), - mcp.Description(widthUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - playlistId, _ = args["playlistId"].(string) - type_, _ = args["type"].(string) - heightRaw, _ := args["height"].(float64) - height = int64(heightRaw) - widthRaw, _ := args["width"].(float64) - width = int64(widthRaw) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + playlistId = input.PlaylistId + type_ = input.Type + height = input.Height + width = input.Width + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlistImage update started") @@ -97,17 +117,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistImage update failed", - "error", err, - "args", args, + ctx, "playlistImage update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistImage update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/playlistItem/BUILD.bazel b/cmd/playlistItem/BUILD.bazel index 6722d37..e769f31 100644 --- a/cmd/playlistItem/BUILD.bazel +++ b/cmd/playlistItem/BUILD.bazel @@ -15,7 +15,8 @@ go_library( "//cmd", "//pkg", "//pkg/playlistItem", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/playlistItem/delete.go b/cmd/playlistItem/delete.go index f3810b1..fedd718 100644 --- a/cmd/playlistItem/delete.go +++ b/cmd/playlistItem/delete.go @@ -3,12 +3,14 @@ package playlistItem import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/playlistItem" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,43 @@ const ( deleteIdsUsage = "IDs of the playlist items to delete" ) +type deleteIn struct { + Ids []string `json:"ids"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistItem-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) playlistItemCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -43,34 +80,11 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "playlistItem-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "playlistItem delete started") @@ -78,17 +92,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistItem delete failed", - "error", err, - "args", args, + ctx, "playlistItem delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistItem delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/playlistItem/insert.go b/cmd/playlistItem/insert.go index f33a21e..b035f9b 100644 --- a/cmd/playlistItem/insert.go +++ b/cmd/playlistItem/insert.go @@ -3,13 +3,15 @@ package playlistItem import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistItem" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,92 @@ const ( insertPidUsage = "The id that YouTube uses to uniquely identify the playlist that the item is in" ) +type insertIn struct { + Title string `json:"title"` + Description string `json:"description"` + Kind string `json:"kind"` + KVideoId string `json:"kVideoId"` + KChannelId string `json:"kChannelId"` + KPlaylistId string `json:"kPlaylistId"` + PlaylistId string `json:"playlistId"` + ChannelId string `json:"channelId"` + Privacy string `json:"privacy"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "title", "description", "kind", "kVideoId", "kChannelId", + "kPlaylistId", "playlistId", "channelId", "privacy", + "onBehalfOfContentOwner", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "kind": { + Type: "string", Enum: []any{"video", "channel", "playlist", ""}, + Description: kindUsage, Default: json.RawMessage(`""`), + }, + "kVideoId": { + Type: "string", Description: kvidUsage, + Default: json.RawMessage(`""`), + }, + "kChannelId": { + Type: "string", Description: kcidUsage, + Default: json.RawMessage(`""`), + }, + "kPlaylistId": { + Type: "string", Description: kpidUsage, + Default: json.RawMessage(`""`), + }, + "playlistId": { + Type: "string", Description: insertPidUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistItem-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) playlistItemCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP(&title, "title", "t", "", titleUsage) @@ -58,79 +144,21 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "playlistItem-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithString( - "kind", mcp.Enum("video", "channel", "playlist"), - mcp.DefaultString(""), mcp.Description(kindUsage), mcp.Required(), - ), - mcp.WithString( - "kVideoId", mcp.DefaultString(""), - mcp.Description(kvidUsage), mcp.Required(), - ), - mcp.WithString( - "kChannelId", mcp.DefaultString(""), - mcp.Description(kcidUsage), mcp.Required(), - ), - mcp.WithString( - "kPlaylistId", mcp.DefaultString(""), - mcp.Description(kpidUsage), mcp.Required(), - ), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(insertPidUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - title, _ = args["title"].(string) - description, _ = args["description"].(string) - kind, _ = args["kind"].(string) - kVideoId, _ = args["kVideoId"].(string) - kChannelId, _ = args["kChannelId"].(string) - kPlaylistId, _ = args["kPlaylistId"].(string) - playlistId, _ = args["playlistId"].(string) - channelId, _ = args["channelId"].(string) - privacy, _ = args["privacy"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + title = input.Title + description = input.Description + kind = input.Kind + kVideoId = input.KVideoId + kChannelId = input.KChannelId + kPlaylistId = input.KPlaylistId + playlistId = input.PlaylistId + channelId = input.ChannelId + privacy = input.Privacy + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlistItem insert started") @@ -138,17 +166,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistItem insert failed", - "error", err, - "args", args, + ctx, "playlistItem insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistItem insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/playlistItem/list.go b/cmd/playlistItem/list.go index 381717c..8537bff 100644 --- a/cmd/playlistItem/list.go +++ b/cmd/playlistItem/list.go @@ -3,13 +3,15 @@ package playlistItem import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistItem" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -20,8 +22,78 @@ const ( listPidUsage = "Return the playlist items within the given playlist" ) +type listIn struct { + Ids []string `json:"ids"` + PlaylistId string `json:"playlistId"` + MaxResults int64 `json:"maxResults"` + VideoId string `json:"videoId"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "playlistId", "maxResults", "videoId", + "onBehalfOfContentOwner", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "playlistId": { + Type: "string", Description: listPidUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "videoId": { + Type: "string", Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet","status"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistItem-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) playlistItemCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -51,70 +123,17 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "playlistItem-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(listPidUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet", "status"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - playlistId, _ = args["playlistId"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - videoId, _ = args["videoId"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + playlistId = input.PlaylistId + maxResults = input.MaxResults + videoId = input.VideoId + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlistItem list started") @@ -122,17 +141,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistItem list failed", - "error", err, - "args", args, + ctx, "playlistItem list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistItem list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/playlistItem/update.go b/cmd/playlistItem/update.go index b9fbdd2..fa33080 100644 --- a/cmd/playlistItem/update.go +++ b/cmd/playlistItem/update.go @@ -3,13 +3,15 @@ package playlistItem import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/playlistItem" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,69 @@ const ( updateIdUsage = "ID of the playlist item to update" ) +type updateIn struct { + Ids []string `json:"ids"` + Title string `json:"title"` + Description string `json:"description"` + Privacy string `json:"privacy"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "title", "description", "privacy", + "onBehalfOfContentOwner", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: updateIdUsage, + Default: json.RawMessage(`[]`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "playlistItem-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) playlistItemCmd.AddCommand(updateCmd) updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage) @@ -49,59 +112,16 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "playlistItem-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(updateIdUsage), mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - title, _ = args["title"].(string) - description, _ = args["description"].(string) - privacy, _ = args["privacy"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + title = input.Title + description = input.Description + privacy = input.Privacy + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "playlistItem update started") @@ -109,17 +129,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "playlistItem update failed", - "error", err, - "args", args, + ctx, "playlistItem update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "playlistItem update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/search/BUILD.bazel b/cmd/search/BUILD.bazel index de3ddc3..fa5b52d 100644 --- a/cmd/search/BUILD.bazel +++ b/cmd/search/BUILD.bazel @@ -13,7 +13,8 @@ go_library( "//pkg", "//pkg/search", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/search/list.go b/cmd/search/list.go index 3ff51ba..941f06d 100644 --- a/cmd/search/list.go +++ b/cmd/search/list.go @@ -3,6 +3,7 @@ package search import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,12 +11,225 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/search" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + ChannelId string `json:"channelId"` + ChannelType string `json:"channelType"` + EventType string `json:"eventType"` + ForContentOwner *string `json:"forContentOwner,omitempty"` + ForDeveloper *string `json:"forDeveloper,omitempty"` + ForMine *string `json:"forMine,omitempty"` + Location string `json:"location"` + LocationRadius string `json:"locationRadius"` + MaxResults int64 `json:"maxResults"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Order string `json:"order"` + PublishedAfter string `json:"publishedAfter"` + PublishedBefore string `json:"publishedBefore"` + Q string `json:"q"` + RegionCode string `json:"regionCode"` + RelevanceLanguage string `json:"relevanceLanguage"` + SafeSearch string `json:"safeSearch"` + TopicId string `json:"topicId"` + Types []string `json:"types"` + VideoCaption string `json:"videoCaption"` + VideoCategoryId string `json:"videoCategoryId"` + VideoDefinition string `json:"videoDefinition"` + VideoDimension string `json:"videoDimension"` + VideoDuration string `json:"videoDuration"` + VideoEmbeddable string `json:"videoEmbeddable"` + VideoLicense string `json:"videoLicense"` + VideoPaidProductPlacement string `json:"videoPaidProductPlacement"` + VideoSyndicated string `json:"videoSyndicated"` + VideoType string `json:"videoType"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "channelId", "channelType", "eventType", "forContentOwner", "forDeveloper", + "forMine", "location", "locationRadius", "maxResults", + "onBehalfOfContentOwner", + "order", "publishedAfter", "publishedBefore", "q", "regionCode", + "relevanceLanguage", "safeSearch", "topicId", "types", "videoCaption", + "videoCategoryId", "videoDefinition", "videoDimension", "videoDuration", + "videoEmbeddable", "videoLicense", "videoPaidProductPlacement", + "videoSyndicated", + "videoType", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "channelId": { + Type: "string", Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "channelType": { + Type: "string", Enum: []any{"channelTypeUnspecified", "any", "show"}, + Description: ctUsage, Default: json.RawMessage(`"channelTypeUnspecified"`), + }, + "eventType": { + Type: "string", Enum: []any{"none", "upcoming", "live", "completed"}, + Description: etUsage, Default: json.RawMessage(`"none"`), + }, + "forContentOwner": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: fcoUsage, Default: json.RawMessage(`""`), + }, + "forDeveloper": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: fdUsage, Default: json.RawMessage(`""`), + }, + "forMine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: fmUsage, Default: json.RawMessage(`""`), + }, + "location": { + Type: "string", Description: locationUsage, + Default: json.RawMessage(`""`), + }, + "locationRadius": { + Type: "string", Description: lrUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "order": { + Type: "string", Description: orderUsage, + Default: json.RawMessage(`"relevance"`), + }, + "publishedAfter": { + Type: "string", Description: paUsage, + Default: json.RawMessage(`""`), + }, + "publishedBefore": { + Type: "string", Description: pbUsage, + Default: json.RawMessage(`""`), + }, + "q": { + Type: "string", Description: qUsage, + Default: json.RawMessage(`""`), + }, + "regionCode": { + Type: "string", Description: rcUsage, + Default: json.RawMessage(`""`), + }, + "relevanceLanguage": { + Type: "string", Description: rlUsage, + Default: json.RawMessage(`""`), + }, + "safeSearch": { + Type: "string", + Enum: []any{ + "safeSearchSettingUnspecified", "none", "moderate", "strict", + }, + Description: ssUsage, Default: json.RawMessage(`"moderate"`), + }, + "topicId": { + Type: "string", Description: tidUsage, + Default: json.RawMessage(`""`), + }, + "types": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: typesUsage, + Default: json.RawMessage(`[]`), + }, + "videoCaption": { + Type: "string", + Enum: []any{ + "videoCaptionUnspecified", "any", "closedCaption", "none", + }, + Description: vcUsage, Default: json.RawMessage(`"any"`), + }, + "videoCategoryId": { + Type: "string", Description: vcidUsage, + Default: json.RawMessage(`""`), + }, + "videoDefinition": { + Type: "string", Description: vdeUsage, + Default: json.RawMessage(`""`), + }, + "videoDimension": { + Type: "string", Enum: []any{"any", "2d", "3d"}, + Description: vdiUsage, Default: json.RawMessage(`"any"`), + }, + "videoDuration": { + Type: "string", + Enum: []any{ + "videoDurationUnspecified", "any", "short", "medium", "long", + }, + Description: vduUsage, Default: json.RawMessage(`"any"`), + }, + "videoEmbeddable": { + Type: "string", + Enum: []any{"videoEmbeddableUnspecified", "any", "true", ""}, + Description: veUsage, Default: json.RawMessage(`""`), + }, + "videoLicense": { + Type: "string", Enum: []any{"any", "youtube", "creativeCommon", ""}, + Description: vlUsage, Default: json.RawMessage(`""`), + }, + "videoPaidProductPlacement": { + Type: "string", + Enum: []any{ + "videoPaidProductPlacementUnspecified", "any", "true", "", + }, + Description: vpppUsage, Default: json.RawMessage(`""`), + }, + "videoSyndicated": { + Type: "string", + Enum: []any{"videoSyndicatedUnspecified", "any", "true", ""}, + Description: vsUsage, Default: json.RawMessage(`""`), + }, + "videoType": { + Type: "string", + Enum: []any{"videoTypeUnspecified", "any", "movie", "episode", ""}, + Description: vtUsage, Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "search-list", Title: short, Description: long, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) searchCmd.AddCommand(listCmd) listCmd.Flags().StringVar(&channelId, "channelId", "", cidUsage) @@ -75,199 +289,41 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "search-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "channelType", - mcp.Enum("channelTypeUnspecified", "any", "show"), - mcp.DefaultString("channelTypeUnspecified"), - mcp.Description(ctUsage), mcp.Required(), - ), - mcp.WithString( - "eventType", mcp.Enum("none", "upcoming", "live", "completed"), - mcp.DefaultString("none"), mcp.Description(etUsage), mcp.Required(), - ), - mcp.WithString( - "forContentOwner", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(fcoUsage), mcp.Required(), - ), - mcp.WithString( - "forDeveloper", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(fdUsage), mcp.Required(), - ), - mcp.WithString( - "forMine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(fmUsage), mcp.Required(), - ), - mcp.WithString( - "location", mcp.DefaultString(""), - mcp.Description(locationUsage), mcp.Required(), - ), - mcp.WithString( - "locationRadius", mcp.DefaultString(""), - mcp.Description(lrUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "order", mcp.DefaultString("relevance"), - mcp.Description(orderUsage), mcp.Required(), - ), - mcp.WithString( - "publishedAfter", mcp.DefaultString(""), - mcp.Description(paUsage), mcp.Required(), - ), - mcp.WithString( - "publishedBefore", mcp.DefaultString(""), - mcp.Description(pbUsage), mcp.Required(), - ), - mcp.WithString( - "q", mcp.DefaultString(""), - mcp.Description(qUsage), mcp.Required(), - ), - mcp.WithString( - "regionCode", mcp.DefaultString(""), - mcp.Description(rcUsage), mcp.Required(), - ), - mcp.WithString( - "relevanceLanguage", mcp.DefaultString(""), - mcp.Description(rlUsage), mcp.Required(), - ), - mcp.WithString( - "safeSearch", - mcp.Enum("safeSearchSettingUnspecified", "none", "moderate", "strict"), - mcp.DefaultString("moderate"), mcp.Description(ssUsage), mcp.Required(), - ), - mcp.WithString( - "topicId", mcp.DefaultString(""), - mcp.Description(tidUsage), mcp.Required(), - ), - mcp.WithArray( - "types", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(typesUsage), mcp.Required(), - ), - mcp.WithString( - "videoCaption", - mcp.Enum("videoCaptionUnspecified", "any", "closedCaption", "none"), - mcp.DefaultString("any"), mcp.Description(vcUsage), mcp.Required(), - ), - mcp.WithString( - "videoCategoryId", mcp.DefaultString(""), - mcp.Description(vcidUsage), mcp.Required(), - ), - mcp.WithString( - "videoDefinition", mcp.DefaultString(""), - mcp.Description(vdeUsage), mcp.Required(), - ), - mcp.WithString( - "videoDimension", mcp.Enum("any", "2d", "3d"), - mcp.DefaultString("any"), mcp.Description(vdiUsage), mcp.Required(), - ), - mcp.WithString( - "videoDuration", - mcp.Enum("videoDurationUnspecified", "any", "short", "medium", "long"), - mcp.DefaultString("any"), mcp.Description(vduUsage), mcp.Required(), - ), - mcp.WithString( - "videoEmbeddable", mcp.Enum("videoEmbeddableUnspecified", "any", "true"), - mcp.DefaultString(""), mcp.Description(veUsage), mcp.Required(), - ), - mcp.WithString( - "videoLicense", mcp.Enum("any", "youtube", "creativeCommon"), - mcp.DefaultString(""), mcp.Description(vlUsage), mcp.Required(), - ), - mcp.WithString( - "videoPaidProductPlacement", - mcp.Enum("videoPaidProductPlacementUnspecified", "any", "true"), - mcp.DefaultString(""), mcp.Description(vpppUsage), mcp.Required(), - ), - mcp.WithString( - "videoSyndicated", mcp.Enum("videoSyndicatedUnspecified", "any", "true"), - mcp.DefaultString(""), mcp.Description(vsUsage), mcp.Required(), - ), - mcp.WithString( - "videoType", mcp.Enum("videoTypeUnspecified", "any", "movie", "episode"), - mcp.DefaultString(""), mcp.Description(vtUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - channelId, _ = args["channelId"].(string) - channelType, _ = args["channelType"].(string) - eventType, _ = args["eventType"].(string) - forContentOwnerRaw, _ := args["forContentOwner"].(string) - forContentOwner = utils.BoolPtr(forContentOwnerRaw) - forDeveloperRaw, _ := args["forDeveloper"].(string) - forDeveloper = utils.BoolPtr(forDeveloperRaw) - forMineRaw, _ := args["forMine"].(string) - forMine = utils.BoolPtr(forMineRaw) - location, _ = args["location"].(string) - locationRadius, _ = args["locationRadius"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - order, _ = args["order"].(string) - publishedAfter, _ = args["publishedAfter"].(string) - publishedBefore, _ = args["publishedBefore"].(string) - q, _ = args["q"].(string) - regionCode, _ = args["regionCode"].(string) - relevanceLanguage, _ = args["relevanceLanguage"].(string) - safeSearch, _ = args["safeSearch"].(string) - topicId, _ = args["topicId"].(string) - typesRaw, _ := args["types"].([]any) - types = make([]string, len(typesRaw)) - for i, typ := range typesRaw { - types[i] = typ.(string) - } - videoCaption, _ = args["videoCaption"].(string) - videoCategoryId, _ = args["videoCategoryId"].(string) - videoDefinition, _ = args["videoDefinition"].(string) - videoDimension, _ = args["videoDimension"].(string) - videoDuration, _ = args["videoDuration"].(string) - videoEmbeddable, _ = args["videoEmbeddable"].(string) - videoLicense, _ = args["videoLicense"].(string) - videoPaidProductPlacement, _ = args["videoPaidProductPlacement"].(string) - videoSyndicated, _ = args["videoSyndicated"].(string) - videoType, _ = args["videoType"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + channelId = input.ChannelId + channelType = input.ChannelType + eventType = input.EventType + forContentOwner = utils.BoolPtr(*input.ForContentOwner) + forDeveloper = utils.BoolPtr(*input.ForDeveloper) + forMine = utils.BoolPtr(*input.ForMine) + location = input.Location + locationRadius = input.LocationRadius + maxResults = input.MaxResults + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + order = input.Order + publishedAfter = input.PublishedAfter + publishedBefore = input.PublishedBefore + q = input.Q + regionCode = input.RegionCode + relevanceLanguage = input.RelevanceLanguage + safeSearch = input.SafeSearch + topicId = input.TopicId + types = input.Types + videoCaption = input.VideoCaption + videoCategoryId = input.VideoCategoryId + videoDefinition = input.VideoDefinition + videoDimension = input.VideoDimension + videoDuration = input.VideoDuration + videoEmbeddable = input.VideoEmbeddable + videoLicense = input.VideoLicense + videoPaidProductPlacement = input.VideoPaidProductPlacement + videoSyndicated = input.VideoSyndicated + videoType = input.VideoType + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "search list started") @@ -275,17 +331,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "search list failed", - "error", err, - "args", args, + ctx, "search list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "search list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/subscription/BUILD.bazel b/cmd/subscription/BUILD.bazel index 772b191..e39936b 100644 --- a/cmd/subscription/BUILD.bazel +++ b/cmd/subscription/BUILD.bazel @@ -15,7 +15,8 @@ go_library( "//pkg", "//pkg/subscription", "//pkg/utils", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/subscription/delete.go b/cmd/subscription/delete.go index 0deec77..b92dfe8 100644 --- a/cmd/subscription/delete.go +++ b/cmd/subscription/delete.go @@ -3,12 +3,14 @@ package subscription import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/subscription" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -18,8 +20,36 @@ const ( deleteIdsUsage = "IDs of the subscriptions to delete" ) +type deleteIn struct { + Ids []string `json:"ids"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"ids"}, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + }, +} + func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "subscription-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) subscriptionCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -39,29 +69,10 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "subscription-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids slog.InfoContext(ctx, "subscription delete started") @@ -69,17 +80,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "subscription delete failed", - "error", err, - "args", args, + ctx, "subscription delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "subscription delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/subscription/insert.go b/cmd/subscription/insert.go index c67f57b..a02ffa0 100644 --- a/cmd/subscription/insert.go +++ b/cmd/subscription/insert.go @@ -3,13 +3,15 @@ package subscription import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/subscription" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -19,8 +21,61 @@ const ( insertCidUsage = "ID of the channel to be subscribed" ) +type insertIn struct { + SubscriberChannelId string `json:"subscriberChannelId"` + Description string `json:"description"` + ChannelId string `json:"channelId"` + Title string `json:"title"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "subscriberChannelId", "description", "channelId", "title", + "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "subscriberChannelId": { + Type: "string", Description: scidUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: insertCidUsage, + Default: json.RawMessage(`""`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "subscription-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) subscriptionCmd.AddCommand(insertCmd) insertCmd.Flags().StringVarP( @@ -49,49 +104,15 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "subscription-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "subscriberChannelId", mcp.DefaultString(""), - mcp.Description(scidUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(insertCidUsage), mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - subscriberChannelId, _ = args["subscriberChannelId"].(string) - description, _ = args["description"].(string) - channelId, _ = args["channelId"].(string) - title, _ = args["title"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + subscriberChannelId = input.SubscriberChannelId + description = input.Description + channelId = input.ChannelId + title = input.Title + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "subscription insert started") @@ -99,17 +120,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "subscription insert failed", - "error", err, - "args", args, + ctx, "subscription insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "subscription insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/subscription/list.go b/cmd/subscription/list.go index 2dc55e9..f474fb3 100644 --- a/cmd/subscription/list.go +++ b/cmd/subscription/list.go @@ -3,6 +3,7 @@ package subscription import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,7 +11,8 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/subscription" "github.com/eat-pray-ai/yutu/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -21,8 +23,106 @@ const ( listCidUsage = "Return the subscriptions of the given channel owner" ) +type listIn struct { + Ids []string `json:"ids"` + ChannelId string `json:"channelId"` + ForChannelId string `json:"forChannelId"` + MaxResults int64 `json:"maxResults"` + Mine *string `json:"mine,omitempty"` + MyRecentSubscribers *string `json:"myRecentSubscribers,omitempty"` + MySubscribers *string `json:"mySubscribers,omitempty"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + OnBehalfOfContentOwnerChannel string `json:"onBehalfOfContentOwnerChannel"` + Order string `json:"order"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "channelId", "forChannelId", "maxResults", "mine", + "myRecentSubscribers", "mySubscribers", "onBehalfOfContentOwner", + "onBehalfOfContentOwnerChannel", "order", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "channelId": { + Type: "string", Description: listCidUsage, + Default: json.RawMessage(`""`), + }, + "forChannelId": { + Type: "string", Description: fcidUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "mine": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mineUsage, Default: json.RawMessage(`""`), + }, + "myRecentSubscribers": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: mrsUsage, Default: json.RawMessage(`""`), + }, + "mySubscribers": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: msUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwnerChannel": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "order": { + Type: "string", Enum: []any{ + "subscriptionOrderUnspecified", "relevance", "unread", "alphabetical", + }, + Description: orderUsage, Default: json.RawMessage(`"relevance"`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "subscription-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) subscriptionCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -61,103 +161,22 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "subscription-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(listCidUsage), mcp.Required(), - ), - mcp.WithString( - "forChannelId", mcp.DefaultString(""), - mcp.Description(fcidUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "mine", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mineUsage), mcp.Required(), - ), - mcp.WithString( - "myRecentSubscribers", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(mrsUsage), mcp.Required(), - ), - mcp.WithString( - "mySubscribers", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(msUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwnerChannel", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "order", mcp.Enum( - "subscriptionOrderUnspecified", "relevance", "unread", "alphabetical", - ), - mcp.DefaultString("relevance"), mcp.Description(orderUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - channelId, _ = args["channelId"].(string) - forChannelId, _ = args["forChannelId"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - mineRaw, ok := args["mine"].(string) - if !ok { - mineRaw = "true" - } - mine = utils.BoolPtr(mineRaw) - myRecentSubscribersRaw, _ := args["myRecentSubscribers"].(string) - myRecentSubscribers = utils.BoolPtr(myRecentSubscribersRaw) - mySubscribersRaw, _ := args["mySubscribers"].(string) - mySubscribers = utils.BoolPtr(mySubscribersRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - onBehalfOfContentOwnerChannel, _ = args["onBehalfOfContentOwnerChannel"].(string) - order, _ = args["order"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + channelId = input.ChannelId + forChannelId = input.ForChannelId + maxResults = input.MaxResults + mine = utils.BoolPtr(*input.Mine) + myRecentSubscribers = utils.BoolPtr(*input.MyRecentSubscribers) + mySubscribers = utils.BoolPtr(*input.MySubscribers) + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + onBehalfOfContentOwnerChannel = input.OnBehalfOfContentOwnerChannel + order = input.Order + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "subscription list started") @@ -165,17 +184,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "subscription list failed", - "error", err, - "args", args, + ctx, "subscription list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "subscription list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/superChatEvent/BUILD.bazel b/cmd/superChatEvent/BUILD.bazel index ceeb95e..9e1a1c5 100644 --- a/cmd/superChatEvent/BUILD.bazel +++ b/cmd/superChatEvent/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/superChatEvent", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/superChatEvent/list.go b/cmd/superChatEvent/list.go index 3cdc793..f71f164 100644 --- a/cmd/superChatEvent/list.go +++ b/cmd/superChatEvent/list.go @@ -3,18 +3,71 @@ package superChatEvent import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/superChatEvent" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + Hl string `json:"hl"` + MaxResults int64 `json:"maxResults"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "hl", "maxResults", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "hl": { + Type: "string", Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "superChatEvent-list", Title: short, Description: long, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) superChatEventCmd.AddCommand(listCmd) listCmd.Flags().StringVarP(&hl, "hl", "l", "", hlUsage) @@ -39,50 +92,14 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "superChatEvent-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - hl, _ = args["hl"].(string) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + hl = input.Hl + maxResults = input.MaxResults + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "superChatEvent list started") @@ -90,17 +107,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "superChatEvent list failed", - "error", err, - "args", args, + ctx, "superChatEvent list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "superChatEvent list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/thumbnail/BUILD.bazel b/cmd/thumbnail/BUILD.bazel index 8302cba..0e6b6ba 100644 --- a/cmd/thumbnail/BUILD.bazel +++ b/cmd/thumbnail/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/thumbnail", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/thumbnail/set.go b/cmd/thumbnail/set.go index 2c5eb5f..338153f 100644 --- a/cmd/thumbnail/set.go +++ b/cmd/thumbnail/set.go @@ -3,18 +3,62 @@ package thumbnail import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/thumbnail" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type setIn struct { + File string `json:"file"` + VideoId string `json:"videoId"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var setInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "file", "videoId", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "file": { + Type: "string", Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "videoId": { + Type: "string", Description: vidUsage, + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(setTool, setHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "thumbnail-set", Title: short, Description: long, + InputSchema: setInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, setHandler, + ) thumbnailCmd.AddCommand(setCmd) setCmd.Flags().StringVarP(&file, "file", "f", "", fileUsage) @@ -39,39 +83,13 @@ var setCmd = &cobra.Command{ }, } -var setTool = mcp.NewTool( - "thumbnail-set", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "videoId", mcp.DefaultString(""), - mcp.Description(vidUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func setHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - file, _ = args["file"].(string) - videoId, _ = args["videoId"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input setIn, +) (*mcp.CallToolResult, any, error) { + file = input.File + videoId = input.VideoId + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "thumbnail set started") @@ -79,17 +97,15 @@ func setHandler( err := set(&writer) if err != nil { slog.ErrorContext( - ctx, "thumbnail set failed", - "error", err, - "args", args, + ctx, "thumbnail set failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "thumbnail set completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func set(writer io.Writer) error { diff --git a/cmd/video/BUILD.bazel b/cmd/video/BUILD.bazel index 41b7f26..a177d5d 100644 --- a/cmd/video/BUILD.bazel +++ b/cmd/video/BUILD.bazel @@ -19,7 +19,8 @@ go_library( "//pkg", "//pkg/utils", "//pkg/video", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_pflag//:pflag", ], diff --git a/cmd/video/delete.go b/cmd/video/delete.go index 09303d0..2eb70b8 100644 --- a/cmd/video/delete.go +++ b/cmd/video/delete.go @@ -3,15 +3,35 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type deleteIn struct { + Ids []string `json:"ids"` +} + +var deleteInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"ids"}, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: deleteIdsUsage, + Default: json.RawMessage(`[]`), + }, + }, +} + const ( deleteShort = "Delete a video on YouTube" deleteLong = "Delete a video on YouTube by ids" @@ -19,7 +39,17 @@ const ( ) func init() { - cmd.MCP.AddTool(deleteTool, deleteHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-delete", Title: deleteShort, Description: deleteLong, + InputSchema: deleteInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, deleteHandler, + ) videoCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) @@ -39,29 +69,10 @@ var deleteCmd = &cobra.Command{ }, } -var deleteTool = mcp.NewTool( - "video-delete", - mcp.WithTitleAnnotation(deleteShort), - mcp.WithDescription(deleteLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(deleteIdsUsage), mcp.Required(), - ), -) - func deleteHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } + ctx context.Context, _ *mcp.CallToolRequest, input deleteIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids slog.InfoContext(ctx, "video delete started") @@ -69,17 +80,15 @@ func deleteHandler( err := del(&writer) if err != nil { slog.ErrorContext( - ctx, "video delete failed", - "error", err, - "args", args, + ctx, "video delete failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video delete completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func del(writer io.Writer) error { diff --git a/cmd/video/getRating.go b/cmd/video/getRating.go index f81b956..c759593 100644 --- a/cmd/video/getRating.go +++ b/cmd/video/getRating.go @@ -3,16 +3,53 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type getRatingIn struct { + Ids []string `json:"ids"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var getRatingInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "onBehalfOfContentOwner", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: grIdsUsage, + Default: json.RawMessage(`[]`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( getRatingShort = "Get the rating of videos" getRatingLong = "Get the rating of videos by ids" @@ -20,7 +57,17 @@ const ( ) func init() { - cmd.MCP.AddTool(getRatingTool, getRatingHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-getRating", Title: getRatingShort, Description: getRatingLong, + InputSchema: getRatingInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, getRatingHandler, + ) videoCmd.AddCommand(getRatingCmd) getRatingCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, grIdsUsage) @@ -46,44 +93,13 @@ var getRatingCmd = &cobra.Command{ }, } -var getRatingTool = mcp.NewTool( - "video-getRating", - mcp.WithTitleAnnotation(getRatingShort), - mcp.WithDescription(getRatingLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(grIdsUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func getRatingHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input getRatingIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "video getRating started") @@ -91,17 +107,15 @@ func getRatingHandler( err := getRating(&writer) if err != nil { slog.ErrorContext( - ctx, "video getRating failed", - "error", err, - "args", args, + ctx, "video getRating failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video getRating completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func getRating(writer io.Writer) error { diff --git a/cmd/video/insert.go b/cmd/video/insert.go index 637447c..20a01dd 100644 --- a/cmd/video/insert.go +++ b/cmd/video/insert.go @@ -3,6 +3,7 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,10 +11,141 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type insertIn struct { + AutoLevels *string `json:"autoLevels,omitempty"` + File string `json:"file"` + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags"` + Language string `json:"language"` + License string `json:"license"` + Thumbnail string `json:"thumbnail"` + ChannelId string `json:"channelId"` + PlaylistId string `json:"playlistId"` + CategoryId string `json:"categoryId"` + Privacy string `json:"privacy"` + ForKids *string `json:"forKids,omitempty"` + Embeddable *string `json:"embeddable,omitempty"` + PublishAt string `json:"publishAt"` + Stabilize *string `json:"stabilize,omitempty"` + NotifySubscribers *string `json:"notifySubscribers,omitempty"` + PublicStatsViewable *string `json:"publicStatsViewable,omitempty"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + OnBehalfOfContentOwnerChannel string `json:"onBehalfOfContentOwnerChannel"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var insertInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "autoLevels", "file", "title", "description", "tags", + "language", "license", "thumbnail", "channelId", "playlistId", + "categoryId", "privacy", "forKids", "embeddable", "publishAt", + "stabilize", "notifySubscribers", "publicStatsViewable", + "onBehalfOfContentOwner", "onBehalfOfContentOwnerChannel", + "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "autoLevels": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: alUsage, Default: json.RawMessage(`""`), + }, + "file": { + Type: "string", Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "tags": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: tagsUsage, + Default: json.RawMessage(`[]`), + }, + "language": { + Type: "string", Description: insertLangUsage, + Default: json.RawMessage(`""`), + }, + "license": { + Type: "string", Enum: []any{"youtube", "creativeCommon"}, + Description: licenseUsage, Default: json.RawMessage(`"youtube"`), + }, + "thumbnail": { + Type: "string", Description: thumbnailUsage, + Default: json.RawMessage(`""`), + }, + "channelId": { + Type: "string", Description: chidUsage, + Default: json.RawMessage(`""`), + }, + "playlistId": { + Type: "string", Description: pidUsage, + Default: json.RawMessage(`""`), + }, + "categoryId": { + Type: "string", Description: caidUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "forKids": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: fkUsage, Default: json.RawMessage(`""`), + }, + "embeddable": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: embeddableUsage, Default: json.RawMessage(`""`), + }, + "publishAt": { + Type: "string", Description: paUsage, + Default: json.RawMessage(`""`), + }, + "stabilize": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: stabilizeUsage, Default: json.RawMessage(`""`), + }, + "notifySubscribers": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: nsUsage, Default: json.RawMessage(`""`), + }, + "publicStatsViewable": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: psvUsage, Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwnerChannel": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( insertShort = "Upload a video to YouTube" insertLong = "Upload a video to YouTube, with the specified title, description, tags, etc" @@ -21,7 +153,17 @@ const ( ) func init() { - cmd.MCP.AddTool(insertTool, insertHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-insert", Title: insertShort, Description: insertLong, + InputSchema: insertInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, insertHandler, + ) videoCmd.AddCommand(insertCmd) insertCmd.Flags().BoolVarP( @@ -77,140 +219,31 @@ var insertCmd = &cobra.Command{ }, } -var insertTool = mcp.NewTool( - "video-insert", - mcp.WithTitleAnnotation(insertShort), - mcp.WithDescription(insertLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "autoLevels", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(alUsage), mcp.Required(), - ), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithArray( - "tags", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(tagsUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(insertLangUsage), mcp.Required(), - ), - mcp.WithString( - "license", mcp.Enum("youtube", "creativeCommon"), - mcp.DefaultString("youtube"), mcp.Description(licenseUsage), mcp.Required(), - ), - mcp.WithString( - "thumbnail", mcp.DefaultString(""), - mcp.Description(thumbnailUsage), mcp.Required(), - ), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(chidUsage), mcp.Required(), - ), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(pidUsage), mcp.Required(), - ), - mcp.WithString( - "categoryId", mcp.DefaultString(""), - mcp.Description(caidUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "forKids", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(fkUsage), mcp.Required(), - ), - mcp.WithString( - "embeddable", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(embeddableUsage), mcp.Required(), - ), - mcp.WithString( - "publishAt", mcp.DefaultString(""), - mcp.Description(paUsage), mcp.Required(), - ), - mcp.WithString( - "stabilize", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(stabilizeUsage), mcp.Required(), - ), - mcp.WithString( - "notifySubscribers", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(nsUsage), mcp.Required(), - ), - mcp.WithString( - "publicStatsViewable", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(psvUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwnerChannel", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func insertHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - autoLevelsRaw, _ := args["autoLevels"].(string) - autoLevels = utils.BoolPtr(autoLevelsRaw) - file, _ = args["file"].(string) - title, _ = args["title"].(string) - description, _ = args["description"].(string) - tagsRaw, _ := args["tags"].([]any) - tags = make([]string, len(tagsRaw)) - for i, tag := range tagsRaw { - tags[i] = tag.(string) - } - language, _ = args["language"].(string) - license, _ = args["license"].(string) - thumbnail, _ = args["thumbnail"].(string) - channelId, _ = args["channelId"].(string) - playListId, _ = args["playlistId"].(string) - categoryId, _ = args["categoryId"].(string) - privacy, _ = args["privacy"].(string) - forKidsRaw, _ := args["forKids"].(string) - forKids = utils.BoolPtr(forKidsRaw) - embeddableRaw, _ := args["embeddable"].(string) - embeddable = utils.BoolPtr(embeddableRaw) - publishAt, _ = args["publishAt"].(string) - stabilizeRaw, _ := args["stabilize"].(string) - stabilize = utils.BoolPtr(stabilizeRaw) - notifySubscribersRaw, _ := args["notifySubscribers"].(string) - notifySubscribers = utils.BoolPtr(notifySubscribersRaw) - publicStatsViewableRaw, _ := args["publicStatsViewable"].(string) - publicStatsViewable = utils.BoolPtr(publicStatsViewableRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - onBehalfOfContentOwnerChannel, _ = args["onBehalfOfContentOwnerChannel"].(string) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input insertIn, +) (*mcp.CallToolResult, any, error) { + autoLevels = utils.BoolPtr(*input.AutoLevels) + file = input.File + title = input.Title + description = input.Description + tags = input.Tags + language = input.Language + license = input.License + thumbnail = input.Thumbnail + channelId = input.ChannelId + playListId = input.PlaylistId + categoryId = input.CategoryId + privacy = input.Privacy + forKids = utils.BoolPtr(*input.ForKids) + embeddable = utils.BoolPtr(*input.Embeddable) + publishAt = input.PublishAt + stabilize = utils.BoolPtr(*input.Stabilize) + notifySubscribers = utils.BoolPtr(*input.NotifySubscribers) + publicStatsViewable = utils.BoolPtr(*input.PublicStatsViewable) + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + onBehalfOfContentOwnerChannel = input.OnBehalfOfContentOwnerChannel + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "video insert started") @@ -218,17 +251,15 @@ func insertHandler( err := insert(&writer) if err != nil { slog.ErrorContext( - ctx, "video insert failed", - "error", err, - "args", args, + ctx, "video insert failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video insert completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func insert(writer io.Writer) error { diff --git a/cmd/video/list.go b/cmd/video/list.go index 03f4edd..68f6aa6 100644 --- a/cmd/video/list.go +++ b/cmd/video/list.go @@ -3,16 +3,111 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + Ids []string `json:"ids"` + Chart string `json:"chart"` + Hl string `json:"hl"` + Locale string `json:"locale"` + VideoCategoryId string `json:"videoCategoryId"` + RegionCode string `json:"regionCode"` + MaxHeight int64 `json:"maxHeight"` + MaxWidth int64 `json:"maxWidth"` + MaxResults int64 `json:"maxResults"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` + MyRating string `json:"myRating"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "chart", "hl", "locale", "videoCategoryId", + "regionCode", "maxHeight", "maxWidth", "maxResults", + "onBehalfOfContentOwner", "myRating", "parts", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: listIdsUsage, + Default: json.RawMessage(`[]`), + }, + "chart": { + Type: "string", Enum: []any{"chartUnspecified", "mostPopular", ""}, + Description: chartUsage, Default: json.RawMessage(`""`), + }, + "hl": { + Type: "string", Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "locale": { + Type: "string", Description: localUsage, + Default: json.RawMessage(`""`), + }, + "videoCategoryId": { + Type: "string", Description: caidUsage, + Default: json.RawMessage(`""`), + }, + "regionCode": { + Type: "string", Description: rcUsage, + Default: json.RawMessage(`""`), + }, + "maxHeight": { + Type: "number", Description: mhUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "maxWidth": { + Type: "number", Description: mwUsage, + Default: json.RawMessage("0"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "maxResults": { + Type: "number", Description: pkg.MRUsage, + Default: json.RawMessage("5"), + Minimum: jsonschema.Ptr(float64(0)), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + "myRating": { + Type: "string", Description: listMrUsage, + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet","status"]`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( listShort = "List video's info" listLong = "List video's info, such as title, description, etc" @@ -21,7 +116,17 @@ const ( ) func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-list", Title: listShort, Description: listLong, + InputSchema: listInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) videoCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, listIdsUsage) @@ -57,102 +162,23 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "video-list", - mcp.WithTitleAnnotation(listShort), - mcp.WithDescription(listLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(listIdsUsage), mcp.Required(), - ), - mcp.WithString( - "chart", mcp.Enum("chartUnspecified", "mostPopular"), - mcp.DefaultString(""), mcp.Description(chartUsage), mcp.Required(), - ), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithString( - "locale", mcp.DefaultString(""), - mcp.Description(localUsage), mcp.Required(), - ), - mcp.WithString( - "videoCategoryId", mcp.DefaultString(""), - mcp.Description(caidUsage), mcp.Required(), - ), - mcp.WithString( - "regionCode", mcp.DefaultString(""), - mcp.Description(rcUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxHeight", mcp.DefaultNumber(0), - mcp.Description(mhUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxWidth", mcp.DefaultNumber(0), - mcp.Description(mwUsage), mcp.Required(), - ), - mcp.WithNumber( - "maxResults", mcp.DefaultNumber(5), - mcp.Description(pkg.MRUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), - mcp.WithString( - "myRating", mcp.DefaultString(""), - mcp.Description(listMrUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet", "status"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - chart, _ = args["chart"].(string) - hl, _ = args["hl"].(string) - locale, _ = args["locale"].(string) - categoryId, _ = args["videoCategoryId"].(string) - regionCode, _ = args["regionCode"].(string) - maxHeightRaw, _ := args["maxHeight"].(float64) - maxHeight = int64(maxHeightRaw) - maxWidthRaw, _ := args["maxWidth"].(float64) - maxWidth = int64(maxWidthRaw) - maxResultsRaw, _ := args["maxResults"].(float64) - maxResults = int64(maxResultsRaw) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) - rating, _ = args["myRating"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + chart = input.Chart + hl = input.Hl + locale = input.Locale + categoryId = input.VideoCategoryId + regionCode = input.RegionCode + maxHeight = input.MaxHeight + maxWidth = input.MaxWidth + maxResults = input.MaxResults + onBehalfOfContentOwner = input.OnBehalfOfContentOwner + rating = input.MyRating + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "video list started") @@ -160,17 +186,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "video list failed", - "error", err, - "args", args, + ctx, "video list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/video/rate.go b/cmd/video/rate.go index 55db77d..70c3d24 100644 --- a/cmd/video/rate.go +++ b/cmd/video/rate.go @@ -3,15 +3,40 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type rateIn struct { + Ids []string `json:"ids"` + Rating string `json:"rating"` +} + +var rateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"ids", "rating"}, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: rateIdsUsage, + Default: json.RawMessage(`[]`), + }, + "rating": { + Type: "string", Enum: []any{"like", "dislike", "none", ""}, + Description: rateRUsage, Default: json.RawMessage(`""`), + }, + }, +} + const ( rateShort = "Rate a video on YouTube" rateLong = "Rate a video on YouTube, with the specified rating" @@ -20,7 +45,17 @@ const ( ) func init() { - cmd.MCP.AddTool(rateTool, rateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-rate", Title: rateShort, Description: rateLong, + InputSchema: rateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, rateHandler, + ) videoCmd.AddCommand(rateCmd) rateCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, rateIdsUsage) @@ -43,34 +78,11 @@ var rateCmd = &cobra.Command{ }, } -var rateTool = mcp.NewTool( - "video-rate", - mcp.WithTitleAnnotation(rateShort), - mcp.WithDescription(rateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(rateIdsUsage), mcp.Required(), - ), - mcp.WithString( - "rating", mcp.Enum("like", "dislike", "none"), - mcp.DefaultString(""), mcp.Description(rateRUsage), mcp.Required(), - ), -) - func rateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - rating, _ = args["rating"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input rateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + rating = input.Rating slog.InfoContext(ctx, "video rate started") @@ -78,17 +90,15 @@ func rateHandler( err := rate(&writer) if err != nil { slog.ErrorContext( - ctx, "video rate failed", - "error", err, - "args", args, + ctx, "video rate failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video rate completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func rate(writer io.Writer) error { diff --git a/cmd/video/reportAbuse.go b/cmd/video/reportAbuse.go index bfd7fcf..7f72c9b 100644 --- a/cmd/video/reportAbuse.go +++ b/cmd/video/reportAbuse.go @@ -3,15 +3,63 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type reportAbuseIn struct { + Ids []string `json:"ids"` + ReasonId string `json:"reasonId"` + SecondaryReasonId string `json:"secondaryReasonId"` + Comments string `json:"comments"` + Language string `json:"language"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var reportAbuseInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "reasonId", "secondaryReasonId", "comments", + "language", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: raIdsUsage, + Default: json.RawMessage(`[]`), + }, + "reasonId": { + Type: "string", Description: ridUsage, + Default: json.RawMessage(`""`), + }, + "secondaryReasonId": { + Type: "string", Description: sridUsage, + Default: json.RawMessage(`""`), + }, + "comments": { + Type: "string", Description: commentsUsage, + Default: json.RawMessage(`""`), + }, + "language": { + Type: "string", Description: raLangUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + const ( reportAbuseShort = "Report abuse on a video" reportAbuseLong = "Report abuse on a video" @@ -20,7 +68,18 @@ const ( ) func init() { - cmd.MCP.AddTool(reportAbuseTool, reportAbuseHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-reportAbuse", Title: reportAbuseShort, + Description: reportAbuseLong, + InputSchema: reportAbuseInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, reportAbuseHandler, + ) videoCmd.AddCommand(reportAbuseCmd) reportAbuseCmd.Flags().StringSliceVarP( @@ -55,53 +114,15 @@ var reportAbuseCmd = &cobra.Command{ }, } -var reportAbuseTool = mcp.NewTool( - "video-reportAbuse", - mcp.WithTitleAnnotation(reportAbuseShort), - mcp.WithDescription(reportAbuseLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(raIdsUsage), mcp.Required(), - ), - mcp.WithString( - "reasonId", mcp.DefaultString(""), mcp.Description(ridUsage), mcp.Required(), - ), - mcp.WithString( - "secondaryReasonId", mcp.DefaultString(""), mcp.Description(sridUsage), - mcp.Required(), - ), - mcp.WithString( - "comments", mcp.DefaultString(""), mcp.Description(commentsUsage), - mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), mcp.Description(raLangUsage), - mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), mcp.Description(""), - mcp.Required(), - ), -) - func reportAbuseHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - reasonId, _ = args["reasonId"].(string) - secondaryReasonId, _ = args["secondaryReasonId"].(string) - comments, _ = args["comments"].(string) - language, _ = args["language"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input reportAbuseIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + reasonId = input.ReasonId + secondaryReasonId = input.SecondaryReasonId + comments = input.Comments + language = input.Language + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "video reportAbuse started") @@ -109,17 +130,15 @@ func reportAbuseHandler( err := reportAbuse(&writer) if err != nil { slog.ErrorContext( - ctx, "video reportAbuse failed", - "error", err, - "args", args, + ctx, "video reportAbuse failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video reportAbuse completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func reportAbuse(writer io.Writer) error { diff --git a/cmd/video/update.go b/cmd/video/update.go index be2a7e3..9d81fd7 100644 --- a/cmd/video/update.go +++ b/cmd/video/update.go @@ -3,6 +3,7 @@ package video import ( "bytes" "context" + "encoding/json" "io" "log/slog" @@ -10,10 +11,96 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/eat-pray-ai/yutu/pkg/video" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type updateIn struct { + Ids []string `json:"ids"` + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags"` + Language string `json:"language"` + License string `json:"license"` + Thumbnail string `json:"thumbnail"` + PlaylistId string `json:"playlistId"` + CategoryId string `json:"categoryId"` + Privacy string `json:"privacy"` + Embeddable *string `json:"embeddable,omitempty"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var updateInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "ids", "title", "description", "tags", "language", + "license", "thumbnail", "playlistId", "categoryId", + "privacy", "embeddable", "output", "jsonpath", + }, + Properties: map[string]*jsonschema.Schema{ + "ids": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: updateIdUsage, + Default: json.RawMessage(`[]`), + }, + "title": { + Type: "string", Description: titleUsage, + Default: json.RawMessage(`""`), + }, + "description": { + Type: "string", Description: descUsage, + Default: json.RawMessage(`""`), + }, + "tags": { + Type: "array", Items: &jsonschema.Schema{ + Type: "string", + }, + Description: tagsUsage, + Default: json.RawMessage(`[]`), + }, + "language": { + Type: "string", Description: updateLangUsage, + Default: json.RawMessage(`""`), + }, + "license": { + Type: "string", Enum: []any{"youtube", "creativeCommon"}, + Description: licenseUsage, Default: json.RawMessage(`"youtube"`), + }, + "thumbnail": { + Type: "string", Description: thumbnailUsage, + Default: json.RawMessage(`""`), + }, + "playlistId": { + Type: "string", Description: pidUsage, + Default: json.RawMessage(`""`), + }, + "categoryId": { + Type: "string", Description: caidUsage, + Default: json.RawMessage(`""`), + }, + "privacy": { + Type: "string", Enum: []any{"public", "private", "unlisted", ""}, + Description: privacyUsage, Default: json.RawMessage(`""`), + }, + "embeddable": { + Type: "string", Enum: []any{"true", "false", ""}, + Description: embeddableUsage, Default: json.RawMessage(`""`), + }, + "output": { + Type: "string", Enum: []any{"json", "yaml", "silent", ""}, + Description: pkg.SilentUsage, Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + const ( updateShort = "Update a video on YouTube" updateLong = "Update a video on YouTube, with the specified title, description, tags, etc" @@ -22,7 +109,17 @@ const ( ) func init() { - cmd.MCP.AddTool(updateTool, updateHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "video-update", Title: updateShort, Description: updateLong, + InputSchema: updateInSchema, Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: false, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, updateHandler, + ) videoCmd.AddCommand(updateCmd) updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage) @@ -57,95 +154,22 @@ var updateCmd = &cobra.Command{ }, } -var updateTool = mcp.NewTool( - "video-update", - mcp.WithTitleAnnotation(updateShort), - mcp.WithDescription(updateLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithArray( - "ids", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(updateIdUsage), mcp.Required(), - ), - mcp.WithString( - "title", mcp.DefaultString(""), - mcp.Description(titleUsage), mcp.Required(), - ), - mcp.WithString( - "description", mcp.DefaultString(""), - mcp.Description(descUsage), mcp.Required(), - ), - mcp.WithArray( - "tags", mcp.DefaultArray([]string{}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(tagsUsage), mcp.Required(), - ), - mcp.WithString( - "language", mcp.DefaultString(""), - mcp.Description(updateLangUsage), mcp.Required(), - ), - mcp.WithString( - "license", mcp.Enum("youtube", "creativeCommon"), - mcp.DefaultString("youtube"), mcp.Description(licenseUsage), mcp.Required(), - ), - mcp.WithString( - "thumbnail", mcp.DefaultString(""), - mcp.Description(thumbnailUsage), mcp.Required(), - ), - mcp.WithString( - "playlistId", mcp.DefaultString(""), - mcp.Description(pidUsage), mcp.Required(), - ), - mcp.WithString( - "categoryId", mcp.DefaultString(""), - mcp.Description(caidUsage), mcp.Required(), - ), - mcp.WithString( - "privacy", mcp.Enum("public", "private", "unlisted"), - mcp.DefaultString(""), mcp.Description(privacyUsage), mcp.Required(), - ), - mcp.WithString( - "embeddable", mcp.Enum("true", "false", ""), - mcp.DefaultString(""), mcp.Description(embeddableUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "silent", ""), - mcp.DefaultString("yaml"), mcp.Description(pkg.SilentUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func updateHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - idsRaw, _ := args["ids"].([]any) - ids = make([]string, len(idsRaw)) - for i, id := range idsRaw { - ids[i] = id.(string) - } - title, _ = args["title"].(string) - description, _ = args["description"].(string) - tagsRaw, _ := args["tags"].([]any) - tags = make([]string, len(tagsRaw)) - for i, tag := range tagsRaw { - tags[i] = tag.(string) - } - language, _ = args["language"].(string) - license, _ = args["license"].(string) - thumbnail, _ = args["thumbnail"].(string) - playListId, _ = args["playlistId"].(string) - categoryId, _ = args["categoryId"].(string) - privacy, _ = args["privacy"].(string) - embeddableRaw, _ := args["embeddable"].(string) - embeddable = utils.BoolPtr(embeddableRaw) - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input updateIn, +) (*mcp.CallToolResult, any, error) { + ids = input.Ids + title = input.Title + description = input.Description + tags = input.Tags + language = input.Language + license = input.License + thumbnail = input.Thumbnail + playListId = input.PlaylistId + categoryId = input.CategoryId + privacy = input.Privacy + embeddable = utils.BoolPtr(*input.Embeddable) + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "video update started") @@ -153,17 +177,15 @@ func updateHandler( err := update(&writer) if err != nil { slog.ErrorContext( - ctx, "video update failed", - "error", err, - "args", args, + ctx, "video update failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "video update completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func update(writer io.Writer) error { diff --git a/cmd/videoAbuseReportReason/BUILD.bazel b/cmd/videoAbuseReportReason/BUILD.bazel index 205bac9..56802e2 100644 --- a/cmd/videoAbuseReportReason/BUILD.bazel +++ b/cmd/videoAbuseReportReason/BUILD.bazel @@ -12,7 +12,8 @@ go_library( "//cmd", "//pkg", "//pkg/videoAbuseReportReason", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/videoAbuseReportReason/list.go b/cmd/videoAbuseReportReason/list.go index a37e1aa..58b0be6 100644 --- a/cmd/videoAbuseReportReason/list.go +++ b/cmd/videoAbuseReportReason/list.go @@ -3,18 +3,71 @@ package videoAbuseReportReason import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/videoAbuseReportReason" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) +type listIn struct { + Hl string `json:"hl"` + Parts []string `json:"parts"` + Output string `json:"output"` + Jsonpath string `json:"jsonpath"` +} + +var listInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"hl", "parts", "output", "jsonpath"}, + Properties: map[string]*jsonschema.Schema{ + "hl": { + Type: "string", + Description: hlUsage, + Default: json.RawMessage(`""`), + }, + "parts": { + Type: "array", + Items: &jsonschema.Schema{ + Type: "string", + }, + Description: pkg.PartsUsage, + Default: json.RawMessage(`["id","snippet"]`), + }, + "output": { + Type: "string", + Enum: []any{"json", "yaml", "table"}, + Description: pkg.TableUsage, + Default: json.RawMessage(`"yaml"`), + }, + "jsonpath": { + Type: "string", + Description: pkg.JPUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(listTool, listHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "videoAbuseReportReason-list", + Title: short, + Description: long, + InputSchema: listInSchema, + Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: true, + }, + }, listHandler, + ) videoAbuseReportReasonCmd.AddCommand(listCmd) listCmd.Flags().StringVarP(&hl, "hl", "l", "", hlUsage) @@ -38,44 +91,13 @@ var listCmd = &cobra.Command{ }, } -var listTool = mcp.NewTool( - "videoAbuseReportReason-list", - mcp.WithTitleAnnotation(short), - mcp.WithDescription(long), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithString( - "hl", mcp.DefaultString(""), - mcp.Description(hlUsage), mcp.Required(), - ), - mcp.WithArray( - "parts", mcp.DefaultArray([]string{"id", "snippet"}), - mcp.Items(map[string]any{"type": "string"}), - mcp.Description(pkg.PartsUsage), mcp.Required(), - ), - mcp.WithString( - "output", mcp.Enum("json", "yaml", "table"), - mcp.DefaultString("yaml"), mcp.Description(pkg.TableUsage), mcp.Required(), - ), - mcp.WithString( - "jsonpath", mcp.DefaultString(""), - mcp.Description(pkg.JPUsage), mcp.Required(), - ), -) - func listHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - hl, _ = args["hl"].(string) - partsRaw, _ := args["parts"].([]any) - parts = make([]string, len(partsRaw)) - for i, part := range partsRaw { - parts[i] = part.(string) - } - output, _ = args["output"].(string) - jpath, _ = args["jsonpath"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input listIn, +) (*mcp.CallToolResult, any, error) { + hl = input.Hl + parts = input.Parts + output = input.Output + jpath = input.Jsonpath slog.InfoContext(ctx, "videoAbuseReportReason list started") @@ -83,17 +105,15 @@ func listHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "videoAbuseReportReason list failed", - "error", err, - "args", args, + ctx, "videoAbuseReportReason list failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "videoAbuseReportReason list completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func list(writer io.Writer) error { diff --git a/cmd/videoCategory/BUILD.bazel b/cmd/videoCategory/BUILD.bazel index ae4cdb4..faea874 100644 --- a/cmd/videoCategory/BUILD.bazel +++ b/cmd/videoCategory/BUILD.bazel @@ -13,7 +13,7 @@ go_library( "//pkg", "//pkg/utils", "//pkg/videoCategory", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/videoCategory/list.go b/cmd/videoCategory/list.go index 2c26565..b9ed40b 100644 --- a/cmd/videoCategory/list.go +++ b/cmd/videoCategory/list.go @@ -10,12 +10,23 @@ import ( "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/eat-pray-ai/yutu/pkg/videoCategory" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) func init() { - cmd.MCP.AddResourceTemplate(categoriesResource, categoriesHandler) + cmd.Server.AddResourceTemplate( + &mcp.ResourceTemplate{ + Name: vcName, + Title: short, + Description: long, + MIMEType: pkg.JsonMIME, + URITemplate: vcURI, + Annotations: &mcp.Annotations{ + Audience: []mcp.Role{"user", "assistant"}, Priority: 0.51, + }, + }, categoriesHandler, + ) videoCategoryCmd.AddCommand(listCmd) listCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) listCmd.Flags().StringVarP(&hl, "hl", "l", "", hlUsage) @@ -40,16 +51,9 @@ var listCmd = &cobra.Command{ }, } -var categoriesResource = mcp.NewResourceTemplate( - vcURI, vcName, - mcp.WithTemplateMIMEType(pkg.JsonMIME), - mcp.WithTemplateDescription(long), - mcp.WithTemplateAnnotations([]mcp.Role{"user", "assistant"}, 0.51), -) - func categoriesHandler( - ctx context.Context, request mcp.ReadResourceRequest, -) ([]mcp.ResourceContents, error) { + ctx context.Context, request *mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, error) { parts = defaultParts hl = utils.ExtractHl(request.Params.URI) output = "json" @@ -60,9 +64,7 @@ func categoriesHandler( err := list(&writer) if err != nil { slog.ErrorContext( - ctx, "videoCategory list failed", - "error", err, - "uri", request.Params.URI, + ctx, "videoCategory list failed", "error", err, "uri", request.Params.URI, ) return nil, err } @@ -72,14 +74,13 @@ func categoriesHandler( "resultSize", writer.Len(), ) - contents := []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: request.Params.URI, - MIMEType: pkg.JsonMIME, - Text: writer.String(), + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: request.Params.URI, MIMEType: pkg.JsonMIME, Text: writer.String(), + }, }, - } - return contents, nil + }, nil } func list(writer io.Writer) error { diff --git a/cmd/watermark/BUILD.bazel b/cmd/watermark/BUILD.bazel index 4068110..cec0c1d 100644 --- a/cmd/watermark/BUILD.bazel +++ b/cmd/watermark/BUILD.bazel @@ -12,7 +12,8 @@ go_library( deps = [ "//cmd", "//pkg/watermark", - "@com_github_mark3labs_mcp_go//mcp", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/watermark/set.go b/cmd/watermark/set.go index 62977cd..03272f1 100644 --- a/cmd/watermark/set.go +++ b/cmd/watermark/set.go @@ -3,12 +3,14 @@ package watermark import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/watermark" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -17,8 +19,78 @@ const ( setLong = "Set watermark for channel's video by channel id" ) +type setIn struct { + ChannelId string `json:"channelId"` + File string `json:"file"` + InVideoPosition string `json:"inVideoPosition"` + DurationMs int64 `json:"durationMs"` + OffsetMs int64 `json:"offsetMs"` + OffsetType string `json:"offsetType"` + OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` +} + +var setInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{ + "channelId", "file", "inVideoPosition", "durationMs", + "offsetMs", "offsetType", "onBehalfOfContentOwner", + }, + Properties: map[string]*jsonschema.Schema{ + "channelId": { + Type: "string", + Description: cidUsage, + Default: json.RawMessage(`""`), + }, + "file": { + Type: "string", + Description: fileUsage, + Default: json.RawMessage(`""`), + }, + "inVideoPosition": { + Type: "string", + Enum: []any{"topLeft", "topRight", "bottomLeft", "bottomRight", ""}, + Description: ivpUsage, + Default: json.RawMessage(`""`), + }, + "durationMs": { + Type: "number", + Description: dmUsage, + Default: json.RawMessage("0"), + }, + "offsetMs": { + Type: "number", + Description: omUsage, + Default: json.RawMessage("0"), + }, + "offsetType": { + Type: "string", + Enum: []any{"offsetFromStart", "offsetFromEnd", ""}, + Description: otUsage, + Default: json.RawMessage(`""`), + }, + "onBehalfOfContentOwner": { + Type: "string", + Description: "", + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(setTool, setHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "watermark-set", + Title: setShort, + Description: setLong, + InputSchema: setInSchema, + Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(false), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, setHandler, + ) watermarkCmd.AddCommand(setCmd) setCmd.Flags().StringVarP(&channelId, "channelId", "c", "", cidUsage) @@ -50,57 +122,16 @@ var setCmd = &cobra.Command{ }, } -var setTool = mcp.NewTool( - "watermark-set", - mcp.WithTitleAnnotation(setShort), - mcp.WithDescription(setLong), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), - mcp.WithString( - "file", mcp.DefaultString(""), - mcp.Description(fileUsage), mcp.Required(), - ), - mcp.WithString( - "inVideoPosition", - mcp.Enum("topLeft", "topRight", "bottomLeft", "bottomRight"), - mcp.DefaultString(""), mcp.Description(ivpUsage), mcp.Required(), - ), - mcp.WithNumber( - "durationMs", mcp.DefaultNumber(0), - mcp.Description(dmUsage), mcp.Required(), - ), - mcp.WithNumber( - "offsetMs", mcp.DefaultNumber(0), - mcp.Description(omUsage), mcp.Required(), - ), - mcp.WithString( - "offsetType", mcp.Enum("offsetFromStart", "offsetFromEnd"), - mcp.DefaultString(""), mcp.Description(otUsage), mcp.Required(), - ), - mcp.WithString( - "onBehalfOfContentOwner", mcp.DefaultString(""), - mcp.Description(""), mcp.Required(), - ), -) - func setHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - channelId, _ = args["channelId"].(string) - file, _ = args["file"].(string) - inVideoPosition, _ = args["inVideoPosition"].(string) - durationMsRaw, _ := args["durationMs"].(float64) - durationMs = uint64(durationMsRaw) - offsetMsRaw, _ := args["offsetMs"].(float64) - offsetMs = uint64(offsetMsRaw) - offsetType, _ = args["offsetType"].(string) - onBehalfOfContentOwner, _ = args["onBehalfOfContentOwner"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input setIn, +) (*mcp.CallToolResult, any, error) { + channelId = input.ChannelId + file = input.File + inVideoPosition = input.InVideoPosition + durationMs = uint64(input.DurationMs) + offsetMs = uint64(input.OffsetMs) + offsetType = input.OffsetType + onBehalfOfContentOwner = input.OnBehalfOfContentOwner slog.InfoContext(ctx, "watermark set started") @@ -108,17 +139,15 @@ func setHandler( err := set(&writer) if err != nil { slog.ErrorContext( - ctx, "watermark set failed", - "error", err, - "args", args, + ctx, "watermark set failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "watermark set completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func set(writer io.Writer) error { diff --git a/cmd/watermark/unset.go b/cmd/watermark/unset.go index 7db8561..873b340 100644 --- a/cmd/watermark/unset.go +++ b/cmd/watermark/unset.go @@ -3,12 +3,14 @@ package watermark import ( "bytes" "context" + "encoding/json" "io" "log/slog" "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg/watermark" - "github.com/mark3labs/mcp-go/mcp" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -17,8 +19,37 @@ const ( unsetLong = "Unset watermark for channel's video by channel id" ) +type unsetIn struct { + ChannelId string `json:"channelId"` +} + +var unsetInSchema = &jsonschema.Schema{ + Type: "object", + Required: []string{"channelId"}, + Properties: map[string]*jsonschema.Schema{ + "channelId": { + Type: "string", + Description: cidUsage, + Default: json.RawMessage(`""`), + }, + }, +} + func init() { - cmd.MCP.AddTool(unsetTool, unsetHandler) + mcp.AddTool( + cmd.Server, &mcp.Tool{ + Name: "watermark-unset", + Title: unsetShort, + Description: unsetLong, + InputSchema: unsetInSchema, + Annotations: &mcp.ToolAnnotations{ + DestructiveHint: jsonschema.Ptr(true), + IdempotentHint: true, + OpenWorldHint: jsonschema.Ptr(true), + ReadOnlyHint: false, + }, + }, unsetHandler, + ) watermarkCmd.AddCommand(unsetCmd) unsetCmd.Flags().StringVarP(&channelId, "channelId", "c", "", cidUsage) @@ -38,24 +69,10 @@ var unsetCmd = &cobra.Command{ }, } -var unsetTool = mcp.NewTool( - "watermark-unset", - mcp.WithTitleAnnotation(unsetShort), - mcp.WithDescription(unsetLong), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithString( - "channelId", mcp.DefaultString(""), - mcp.Description(cidUsage), mcp.Required(), - ), -) - func unsetHandler( - ctx context.Context, request mcp.CallToolRequest, -) (*mcp.CallToolResult, error) { - args := request.GetArguments() - channelId, _ = args["channelId"].(string) + ctx context.Context, _ *mcp.CallToolRequest, input unsetIn, +) (*mcp.CallToolResult, any, error) { + channelId = input.ChannelId slog.InfoContext(ctx, "watermark unset started") @@ -63,17 +80,15 @@ func unsetHandler( err := unset(&writer) if err != nil { slog.ErrorContext( - ctx, "watermark unset failed", - "error", err, - "args", args, + ctx, "watermark unset failed", "error", err, "input", input, ) - return mcp.NewToolResultError(err.Error()), err + return nil, nil, err } slog.InfoContext( ctx, "watermark unset completed successfully", "resultSize", writer.Len(), ) - return mcp.NewToolResultText(writer.String()), nil + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } func unset(writer io.Writer) error { diff --git a/go.mod b/go.mod index 20e98de..9fe1af9 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/eat-pray-ai/yutu -go 1.25.0 +go 1.25.1 require ( - github.com/mark3labs/mcp-go v0.39.1 + github.com/google/jsonschema-go v0.3.0 github.com/savioxavier/termlink v1.4.3 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 @@ -13,8 +13,6 @@ require ( require ( cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -23,40 +21,37 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/net v0.43.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect - google.golang.org/grpc v1.75.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/grpc v1.75.1 // indirect ) require ( - cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/modelcontextprotocol/go-sdk v0.7.0 github.com/ohler55/ojg v1.26.10 github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 github.com/subosito/gotenv v1.6.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/api v0.249.0 - google.golang.org/protobuf v1.36.8 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/api v0.250.0 + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index c67b921..eda5e46 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,8 @@ cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,6 +24,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -38,20 +36,16 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.39.1 h1:2oPxk7aDbQhouakkYyKl2T4hKFU1c6FDaubWyGyVE1k= -github.com/mark3labs/mcp-go v0.39.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modelcontextprotocol/go-sdk v0.7.0 h1:XEQfn3bDx2cAdSUKty3tYEMll5dtRgBUDX88Q65fai0= +github.com/modelcontextprotocol/go-sdk v0.7.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= github.com/ohler55/ojg v1.26.10 h1:qXq8A0AjzwvO+rKJWv9apNVWxyu3He8lgGZZ+AoEdLA= github.com/ohler55/ojg v1.26.10/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -61,15 +55,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/savioxavier/termlink v1.4.3 h1:Gh6vrG7jSn21cRiYdQqFXYcdXfM+Fg14aG487JTfKpA= github.com/savioxavier/termlink v1.4.3/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -85,12 +77,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -105,32 +95,34 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= -google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= +google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= +google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/utils/BUILD.bazel b/pkg/utils/BUILD.bazel index 688821d..39aaec2 100644 --- a/pkg/utils/BUILD.bazel +++ b/pkg/utils/BUILD.bazel @@ -6,6 +6,7 @@ go_library( importpath = "github.com/eat-pray-ai/yutu/pkg/utils", visibility = ["//visibility:public"], deps = [ + "@com_github_modelcontextprotocol_go_sdk//mcp", "@com_github_ohler55_ojg//jp", "@com_github_spf13_pflag//:pflag", "@in_gopkg_yaml_v3//:yaml_v3", diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9ee8c44..04f7a2c 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -11,6 +11,7 @@ import ( "regexp" "runtime" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/ohler55/ojg/jp" "github.com/spf13/pflag" @@ -100,3 +101,10 @@ func ExtractHl(uri string) string { } return "" } + +func Errf(format string, args ...any) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf(format, args...)}}, + IsError: true, + } +}