Skip to content

Commit 6b67d3a

Browse files
committed
💥 Introduce mcp in activity
issue #64
1 parent 8b4acf4 commit 6b67d3a

File tree

27 files changed

+379
-190
lines changed

27 files changed

+379
-190
lines changed

cmd/activity/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ go_library(
1111
deps = [
1212
"//cmd",
1313
"//pkg/activity",
14+
"//pkg/utils",
15+
"@com_github_mark3labs_mcp_go//mcp",
1416
"@com_github_spf13_cobra//:cobra",
1517
],
1618
)

cmd/activity/activity.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package activity
22

33
import (
44
"github.com/eat-pray-ai/yutu/cmd"
5+
"github.com/eat-pray-ai/yutu/pkg/utils"
56
"github.com/spf13/cobra"
67
)
78

89
var (
910
channelId string
10-
home bool
11+
home = utils.BoolPtr("false")
1112
maxResults int64
12-
mine bool
13+
mine = utils.BoolPtr("true")
1314
publishedAfter string
1415
publishedBefore string
1516
regionCode string

cmd/activity/list.go

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,146 @@
11
package activity
22

33
import (
4+
"bytes"
5+
"context"
6+
"github.com/eat-pray-ai/yutu/cmd"
47
"github.com/eat-pray-ai/yutu/pkg/activity"
8+
"github.com/eat-pray-ai/yutu/pkg/utils"
9+
"github.com/mark3labs/mcp-go/mcp"
510
"github.com/spf13/cobra"
11+
"io"
612
)
713

14+
const (
15+
shortDesc = "List YouTube activities"
16+
longDesc = "List YouTube activities, such as likes, favorites, uploads, etc."
17+
channelIdDesc = "ID of the channel"
18+
homeDesc = "true or false"
19+
maxResultsDesc = "The maximum number of items that should be returned"
20+
mineDesc = "true or false"
21+
publishedAfterDesc = "Filter on activities published after this date"
22+
publishedBeforeDesc = "Filter on activities published before this date"
23+
regionCodeDesc = ""
24+
partsDesc = "Comma separated parts"
25+
outputDesc = "json or yaml"
26+
)
27+
28+
var listTool = mcp.NewTool(
29+
"activity.list",
30+
mcp.WithDescription(longDesc),
31+
mcp.WithString(
32+
"channelId", mcp.DefaultString(""), mcp.Description(channelIdDesc),
33+
),
34+
mcp.WithString(
35+
"home", mcp.Enum("true", "false", ""),
36+
mcp.DefaultString(""), mcp.Description(homeDesc),
37+
),
38+
mcp.WithNumber(
39+
"maxResults", mcp.DefaultNumber(5), mcp.Description(maxResultsDesc),
40+
),
41+
mcp.WithString(
42+
"mine", mcp.Enum("true", "false", ""),
43+
mcp.DefaultString("true"), mcp.Description(mineDesc),
44+
),
45+
mcp.WithString(
46+
"publishedAfter", mcp.DefaultString(""), mcp.Description(publishedAfterDesc),
47+
),
48+
mcp.WithString(
49+
"publishedBefore", mcp.DefaultString(""),
50+
mcp.Description(publishedBeforeDesc),
51+
),
52+
mcp.WithString(
53+
"regionCode", mcp.DefaultString(""), mcp.Description(regionCodeDesc),
54+
),
55+
mcp.WithArray(
56+
"parts", mcp.DefaultArray([]string{"id", "snippet", "contentDetails"}),
57+
mcp.Description(partsDesc),
58+
),
59+
mcp.WithString("output", mcp.DefaultString(""), mcp.Description(outputDesc)),
60+
)
61+
62+
func run(writer io.Writer) error {
63+
a := activity.NewActivity(
64+
activity.WithChannelId(channelId),
65+
activity.WithHome(home),
66+
activity.WithMaxResults(maxResults),
67+
activity.WithMine(mine),
68+
activity.WithPublishedAfter(publishedAfter),
69+
activity.WithPublishedBefore(publishedBefore),
70+
activity.WithRegionCode(regionCode),
71+
activity.WithService(nil),
72+
)
73+
74+
return a.List(parts, output, writer)
75+
}
76+
877
var listCmd = &cobra.Command{
978
Use: "list",
10-
Short: "List YouTube activities",
11-
Long: "List YouTube activities, such as likes, favorites, uploads, etc.",
79+
Short: shortDesc,
80+
Long: longDesc,
81+
PreRun: func(cmd *cobra.Command, args []string) {
82+
if !cmd.Flags().Lookup("home").Changed {
83+
home = nil
84+
}
85+
if !cmd.Flags().Lookup("mine").Changed {
86+
mine = nil
87+
}
88+
},
1289
Run: func(cmd *cobra.Command, args []string) {
13-
a := activity.NewActivity(
14-
activity.WithChannelId(channelId),
15-
activity.WithHome(home, true),
16-
activity.WithMaxResults(maxResults),
17-
activity.WithMine(mine, true),
18-
activity.WithPublishedAfter(publishedAfter),
19-
activity.WithPublishedBefore(publishedBefore),
20-
activity.WithRegionCode(regionCode),
21-
activity.WithService(nil),
22-
)
23-
a.List(parts, output)
90+
err := run(cmd.OutOrStdout())
91+
if err != nil {
92+
_ = cmd.Help()
93+
cmd.PrintErrf("Error: %v\n", err)
94+
}
2495
},
2596
}
2697

2798
func init() {
99+
cmd.MCP.AddTool(listTool, listHandler)
28100
activityCmd.AddCommand(listCmd)
29101
listCmd.Flags().StringVarP(
30-
&channelId, "channelId", "c", "", "ID of the channel",
102+
&channelId, "channelId", "c", "", channelIdDesc,
31103
)
32-
listCmd.Flags().BoolVarP(&home, "home", "H", true, "true or false")
104+
listCmd.Flags().BoolVarP(home, "home", "H", true, homeDesc)
33105
listCmd.Flags().Int64VarP(
34-
&maxResults, "maxResults", "n", 5, "The maximum number of items that should be returned",
106+
&maxResults, "maxResults", "n", 5, maxResultsDesc,
35107
)
36-
listCmd.Flags().BoolVarP(&mine, "mine", "M", true, "true or false")
108+
listCmd.Flags().BoolVarP(mine, "mine", "M", true, mineDesc)
37109
listCmd.Flags().StringVarP(
38-
&publishedAfter, "publishedAfter", "a", "",
39-
"Filter on activities published after this date",
110+
&publishedAfter, "publishedAfter", "a", "", publishedAfterDesc,
40111
)
41112
listCmd.Flags().StringVarP(
42-
&publishedBefore, "publishedBefore", "b", "",
43-
"Filter on activities published before this date",
113+
&publishedBefore, "publishedBefore", "b", "", publishedBeforeDesc,
44114
)
45-
listCmd.Flags().StringVarP(&regionCode, "regionCode", "r", "", "")
115+
listCmd.Flags().StringVarP(&regionCode, "regionCode", "r", "", regionCodeDesc)
46116

47117
listCmd.Flags().StringArrayVarP(
48-
&parts, "parts", "p", []string{"id", "snippet", "contentDetails"},
49-
"Comma separated parts",
50-
)
51-
listCmd.Flags().StringVarP(
52-
&output, "output", "o", "", "json or yaml",
118+
&parts, "parts", "p", []string{"id", "snippet", "contentDetails"}, partsDesc,
53119
)
120+
listCmd.Flags().StringVarP(&output, "output", "o", "", outputDesc)
121+
}
122+
123+
func listHandler(ctx context.Context, request mcp.CallToolRequest) (
124+
*mcp.CallToolResult, error,
125+
) {
126+
args := request.GetArguments()
127+
channelId = args["channelId"].(string)
128+
home = utils.BoolPtr(args["home"].(string))
129+
maxResults = int64(args["maxResults"].(float64))
130+
mine = utils.BoolPtr(args["mine"].(string))
131+
publishedAfter = args["publishedAfter"].(string)
132+
publishedBefore = args["publishedBefore"].(string)
133+
regionCode = args["regionCode"].(string)
134+
parts = make([]string, len(args["parts"].([]interface{})))
135+
for i, part := range args["parts"].([]interface{}) {
136+
parts[i] = part.(string)
137+
}
138+
output = args["output"].(string)
139+
140+
var writer bytes.Buffer
141+
err := run(&writer)
142+
if err != nil {
143+
return mcp.NewToolResultError(err.Error()), err
144+
}
145+
return mcp.NewToolResultText(writer.String()), nil
54146
}

cmd/mcp.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"github.com/mark3labs/mcp-go/server"
6+
"github.com/spf13/cobra"
7+
"log"
8+
"time"
9+
)
10+
11+
const (
12+
modeUsage = "stdio, http or sse"
13+
portUsage = "Port to listen on for HTTP or SSE mode"
14+
)
15+
16+
var (
17+
mode string
18+
port int
19+
)
20+
21+
var MCP = server.NewMCPServer(
22+
"yutu", Version,
23+
server.WithToolCapabilities(true),
24+
server.WithRecovery(),
25+
)
26+
27+
var mcpCmd = &cobra.Command{
28+
Use: "mcp",
29+
Short: "Start mcp server",
30+
Long: "Start mcp server to handle requests from clients",
31+
Run: func(cmd *cobra.Command, args []string) {
32+
var err error
33+
interval := 13 * time.Second
34+
addr := fmt.Sprintf(":%d", port)
35+
baseURL := fmt.Sprintf("http://localhost:%d", port)
36+
message := fmt.Sprintf("Starting MCP server: %s", baseURL)
37+
38+
switch mode {
39+
case "stdio":
40+
err = server.ServeStdio(MCP)
41+
case "http":
42+
httpServer := server.NewStreamableHTTPServer(
43+
MCP,
44+
server.WithHeartbeatInterval(interval),
45+
)
46+
log.Printf("%s/%s\n", message, "mcp")
47+
err = httpServer.Start(addr)
48+
case "sse":
49+
sse := server.NewSSEServer(
50+
MCP, server.WithBaseURL(baseURL),
51+
server.WithKeepAlive(true),
52+
server.WithKeepAliveInterval(interval),
53+
)
54+
log.Printf("%s/%s\n", message, "sse")
55+
err = sse.Start(addr)
56+
}
57+
58+
if err != nil {
59+
fmt.Printf("Server error: %v\n", err)
60+
}
61+
},
62+
}
63+
64+
func init() {
65+
RootCmd.AddCommand(mcpCmd)
66+
67+
mcpCmd.Flags().StringVarP(&mode, "mode", "m", "stdio", modeUsage)
68+
mcpCmd.Flags().IntVarP(&port, "port", "p", 8080, portUsage)
69+
}

pkg/activity/activity.go

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import (
44
"errors"
55
"fmt"
66
"github.com/eat-pray-ai/yutu/pkg/auth"
7-
"log"
8-
97
"github.com/eat-pray-ai/yutu/pkg/utils"
108
"google.golang.org/api/youtube/v3"
9+
"io"
1110
)
1211

1312
var (
@@ -26,8 +25,8 @@ type activity struct {
2625
}
2726

2827
type Activity interface {
29-
List([]string, string)
30-
get([]string) []*youtube.Activity
28+
List([]string, string, io.Writer) error
29+
get([]string) ([]*youtube.Activity, error)
3130
}
3231

3332
type Option func(*activity)
@@ -42,7 +41,7 @@ func NewActivity(opts ...Option) Activity {
4241
return a
4342
}
4443

45-
func (a *activity) get(parts []string) []*youtube.Activity {
44+
func (a *activity) get(parts []string) ([]*youtube.Activity, error) {
4645
call := service.Activities.List(parts)
4746
if a.ChannelId != "" {
4847
call = call.ChannelId(a.ChannelId)
@@ -75,26 +74,36 @@ func (a *activity) get(parts []string) []*youtube.Activity {
7574

7675
res, err := call.Do()
7776
if err != nil {
78-
utils.PrintJSON(a)
79-
log.Fatalln(errors.Join(errGetActivity, err))
77+
return nil, errors.Join(errGetActivity, err)
8078
}
8179

82-
return res.Items
80+
return res.Items, nil
8381
}
8482

85-
func (a *activity) List(parts []string, output string) {
86-
activities := a.get(parts)
83+
func (a *activity) List(
84+
parts []string, output string, writer io.Writer,
85+
) error {
86+
activities, err := a.get(parts)
87+
if err != nil {
88+
return err
89+
}
90+
8791
switch output {
8892
case "json":
89-
utils.PrintJSON(activities)
93+
utils.PrintJSON(activities, writer)
9094
case "yaml":
91-
utils.PrintYAML(activities)
95+
utils.PrintYAML(activities, writer)
9296
default:
93-
fmt.Println("ID\tTitle")
97+
_, _ = fmt.Fprintln(writer, "ID\tTitle\tType")
9498
for _, activity := range activities {
95-
fmt.Printf("%s\t%s\n", activity.Id, activity.Snippet.Title)
99+
_, _ = fmt.Fprintf(
100+
writer, "%s\t%s\t%s\n",
101+
activity.Id, activity.Snippet.Title, activity.Snippet.Type,
102+
)
96103
}
97104
}
105+
106+
return nil
98107
}
99108

100109
func WithChannelId(channelId string) Option {
@@ -103,10 +112,10 @@ func WithChannelId(channelId string) Option {
103112
}
104113
}
105114

106-
func WithHome(home bool, changed bool) Option {
115+
func WithHome(home *bool) Option {
107116
return func(a *activity) {
108-
if changed {
109-
a.Home = &home
117+
if home != nil {
118+
a.Home = home
110119
}
111120
}
112121
}
@@ -117,10 +126,10 @@ func WithMaxResults(maxResults int64) Option {
117126
}
118127
}
119128

120-
func WithMine(mine bool, changed bool) Option {
129+
func WithMine(mine *bool) Option {
121130
return func(a *activity) {
122-
if changed {
123-
a.Mine = &mine
131+
if mine != nil {
132+
a.Mine = mine
124133
}
125134
}
126135
}

0 commit comments

Comments
 (0)