AtlatestRepositorytube

tube / tree / src / tubetools.sgl

1;;; (tube tools) - MCP tool definitions and shared helpers for tube.
2;;;
3;;; Provides MCP tool registration for YouTube and Twitch operations.
4;;; Tools present unified cross-platform data where appropriate.
5
6(define-library (tube tools)
7 (import (sigil core)
8 (sigil string)
9 (sigil dict)
10 (sigil struct)
11 (sigil json)
12 (sigil time)
13 (sigil mcp server)
14 (youtube)
15 (youtube analytics)
16 (youtube live)
17 (youtube playlist)
18 (twitch)
19 (twitch schedule)
20 (twitch analytics)
21 (tube config)
22 (tube youtube)
23 (tube twitch))
24 (export register-tube-tools!
25 default-start-date
26 default-end-date
27 default-now-timestamp
28 tool-analytics
29 tool-schedule
30 tool-go-live
31 tool-chat
32 tool-status)
33 (begin
35 ;;; Compute default start date (30 days ago).
36 ;;; Must be called outside async contexts due to VM yield bug
37 ;;; with current-second.
38 (define (default-start-date)
39 (format-date (- (current-second) (* 30 86400))))
41 ;;; Compute default end date (today).
42 ;;; Must be called outside async contexts due to VM yield bug
43 ;;; with current-second.
44 (define (default-end-date)
45 (format-date (current-second)))
47 ;;; Snapshot the current timestamp for days-based date computation.
48 ;;; Must be called outside async contexts.
49 (define (default-now-timestamp)
50 (current-second))
52 ;;; Check if a platform is enabled based on config and requested platform.
53 (define (platform-enabled? config platform requested)
54 (let ((req (or requested (tube-config-default-platform config))))
55 (or (string=? req "both")
56 (string=? req platform))))
58 ;;; Collect results from enabled platforms. Each provider is a pair
59 ;;; of (platform-name . thunk). Returns results from enabled platforms
60 ;;; that succeed, or the fallback message if none produced data.
61 (define (collect-platform-results config requested providers fallback)
62 (let ((results
63 (filter identity
64 (map (lambda (provider)
65 (let ((platform (car provider))
66 (thunk (cdr provider)))
67 (if (platform-enabled? config platform requested)
68 (guard (exn (else #f))
69 (thunk))
70 #f)))
71 providers))))
72 (if (null? results)
73 fallback
74 (string-join results "\n\n"))))
76 ;; ============================================================
77 ;; Tool Handlers
78 ;; ============================================================
80 (define (tool-upload config params)
81 (let* ((file (dict-ref params file:))
82 (title (dict-ref params title: "Untitled"))
83 (description (dict-ref params description: ""))
84 (privacy (dict-ref params privacy: "private")))
85 (unless file
86 (error "file parameter is required"))
87 (let* ((port (open-binary-input-file file))
88 (data (read-bytevector-all port)))
89 (close-input-port port)
90 (let ((result (tube-upload-video config data title description privacy)))
91 (string-append "Video uploaded successfully.\n"
92 "Video ID: " (youtube-video-id result) "\n"
93 "Title: " (youtube-video-title result) "\n"
94 "Privacy: " privacy)))))
96 (define (tool-videos config params)
97 (let* ((limit (dict-ref params limit: 10))
98 (videos (tube-list-videos config limit)))
99 (if (null? videos)
100 "No videos found."
101 (string-append
102 "Recent videos (" (number->string (length videos)) "):\n\n"
103 (string-join (map format-video videos) "\n\n")))))
105 (define (tool-analytics config params start-default end-default
106 now-timestamp)
107 (let* ((platform (dict-ref params platform:
108 (tube-config-default-platform config)))
109 (days (dict-ref params days: #f))
110 (start-date (dict-ref params start_date:
111 (if days
112 (format-date (- now-timestamp
113 (* days 86400)))
114 start-default)))
115 (end-date (dict-ref params end_date: end-default)))
116 (collect-platform-results config platform
117 (list
118 (cons "youtube"
119 (lambda ()
120 (let* ((yt-data (tube-video-analytics config start-date end-date))
121 (rows (dict-ref yt-data rows: '())))
122 (if (null? rows)
123 "No YouTube data for this period."
124 (string-append
125 "YouTube Analytics (" start-date " to " end-date "):\n"
126 "Day\tViews\tWatch Min\tAvg Duration\tNew Subs\n"
127 (string-join (map format-analytics-row rows) "\n"))))))
128 (cons "twitch"
129 (lambda ()
130 (with-twitch-context config
131 (lambda (client broadcaster-id)
132 (let* ((followers (twitch-followers client broadcaster-id
133 first: 1))
134 (total (dict-ref followers total: 0))
135 (clips (twitch-clips client
136 broadcaster-id: broadcaster-id
137 first: 5)))
138 (string-append
139 "Twitch Analytics:\n"
140 "Total Followers: " (number->string total) "\n"
141 (if (null? clips)
142 "No recent clips."
143 (string-append
144 "Recent Clips:\n"
145 (string-join (map format-twitch-clip clips)
146 "\n"))))))))))
147 "No analytics data available. Check platform credentials.")))
149 (define (tool-schedule config params)
150 (let ((platform (dict-ref params platform:
151 (tube-config-default-platform config))))
152 (collect-platform-results config platform
153 (list
154 (cons "youtube"
155 (lambda ()
156 (let ((broadcasts (tube-list-broadcasts config "upcoming")))
157 (if (null? broadcasts)
158 "YouTube: No upcoming broadcasts."
159 (string-append
160 "YouTube Upcoming Broadcasts:\n"
161 (string-join (map format-broadcast broadcasts) "\n"))))))
162 (cons "twitch"
163 (lambda ()
164 (let ((segments (tube-twitch-schedule config)))
165 (if (null? segments)
166 "Twitch: No scheduled streams."
167 (string-append
168 "Twitch Schedule:\n"
169 (string-join (map format-schedule-segment segments) "\n")))))))
170 "No schedule data available. Check platform credentials.")))
172 (define (tool-go-live config params . rest)
173 (let* ((platform (dict-ref params platform:
174 (tube-config-default-platform config)))
175 (title (dict-ref params title: "Live Stream"))
176 ;; Use pre-computed date to avoid calling current-second
177 ;; inside async context (VM yield bug)
178 (today (if (pair? rest) (car rest) (default-end-date))))
179 (collect-platform-results config platform
180 (list
181 (cons "youtube"
182 (lambda ()
183 (let* ((scheduled (string-append today "T00:00:00Z"))
184 (broadcast (tube-create-broadcast config title scheduled)))
185 (string-append
186 "YouTube broadcast created: "
187 (youtube-broadcast-id broadcast)))))
188 (cons "twitch"
189 (lambda ()
190 (with-twitch-context config
191 (lambda (client broadcaster-id)
192 (twitch-modify-channel client broadcaster-id
193 #{ title: title })
194 (string-append "Twitch channel title set to: " title))))))
195 "Failed to start broadcast. Check platform credentials.")))
197 (define (tool-chat config params)
198 (let* ((message (dict-ref params message:))
199 (platform (dict-ref params platform: "twitch")))
200 (unless message
201 (error "message parameter is required"))
202 (cond
203 ((string=? platform "twitch")
204 (tube-twitch-send-chat config message)
205 (string-append "Chat message sent to Twitch: " message))
206 ((string=? platform "youtube")
207 "YouTube live chat is not yet supported via this tool.")
208 (else
209 (error (string-append "Unknown platform: " platform))))))
211 (define (tool-status config params)
212 (let ((platform (dict-ref params platform:
213 (tube-config-default-platform config))))
214 (collect-platform-results config platform
215 (list
216 (cons "youtube" (lambda () (tube-youtube-status config)))
217 (cons "twitch" (lambda () (tube-twitch-status config))))
218 "No platform data available. Check credentials.")))
220 ;; ============================================================
221 ;; MCP Registration
222 ;; ============================================================
224 ;;; Register all tube MCP tools on the given server.
225 ;;; Precomputes default dates here (before async context) to avoid
226 ;;; the VM yield bug with current-second in async goroutines.
227 (define (register-tube-tools! server config)
228 (let ((start-default (default-start-date))
229 (end-default (default-end-date))
230 (now-ts (default-now-timestamp)))
232 (mcp-server-register-tool! server
233 name: "tube/upload"
234 description: "Upload a video to YouTube. Provide the file path, title, optional description, and privacy level."
235 schema: #{ type: "object"
236 properties: #{
237 file: #{ type: "string"
238 description: "Path to the video file to upload" }
239 title: #{ type: "string"
240 description: "Video title" }
241 description: #{ type: "string"
242 description: "Video description" }
243 privacy: #{ type: "string"
244 description: "Privacy status: public, private, or unlisted (default: private)"
245 enum: #["public" "private" "unlisted"] } }
246 required: #["file"] }
247 handler: (lambda (params) (tool-upload config params)))
249 (mcp-server-register-tool! server
250 name: "tube/videos"
251 description: "List recent YouTube videos with view counts, likes, and comment counts."
252 schema: #{ type: "object"
253 properties: #{
254 limit: #{ type: "integer"
255 description: "Number of videos to return (default: 10)" } } }
256 handler: (lambda (params) (tool-videos config params)))
258 (mcp-server-register-tool! server
259 name: "tube/analytics"
260 description: "Get analytics summary combining YouTube and Twitch data. YouTube provides day-by-day time-series (views, watch time, subs). Twitch shows aggregate follower counts and recent clips (the Twitch API does not expose queryable time-series analytics)."
261 schema: #{ type: "object"
262 properties: #{
263 days: #{ type: "integer"
264 description: "Number of days to look back (default: 30)" }
265 platform: #{ type: "string"
266 description: "Platform: youtube, twitch, or both (default: both)"
267 enum: #["youtube" "twitch" "both"] }
268 start_date: #{ type: "string"
269 description: "Start date in YYYY-MM-DD format" }
270 end_date: #{ type: "string"
271 description: "End date in YYYY-MM-DD format" } } }
272 handler: (lambda (params) (tool-analytics config params start-default end-default now-ts)))
274 (mcp-server-register-tool! server
275 name: "tube/schedule"
276 description: "View stream schedule across YouTube (upcoming broadcasts) and Twitch (schedule segments)."
277 schema: #{ type: "object"
278 properties: #{
279 platform: #{ type: "string"
280 description: "Platform: youtube, twitch, or both (default: both)"
281 enum: #["youtube" "twitch" "both"] } } }
282 handler: (lambda (params) (tool-schedule config params)))
284 (mcp-server-register-tool! server
285 name: "tube/go-live"
286 description: "Start a broadcast on one or both platforms. Creates a YouTube broadcast and/or sets the Twitch channel title."
287 schema: #{ type: "object"
288 properties: #{
289 title: #{ type: "string"
290 description: "Stream title" }
291 platform: #{ type: "string"
292 description: "Platform: youtube, twitch, or both (default: both)"
293 enum: #["youtube" "twitch" "both"] } } }
294 handler: (lambda (params) (tool-go-live config params end-default)))
296 (mcp-server-register-tool! server
297 name: "tube/chat"
298 description: "Send a message to live chat. Currently supports Twitch only. YouTube live chat requires an active broadcast and is not yet implemented."
299 schema: #{ type: "object"
300 properties: #{
301 message: #{ type: "string"
302 description: "Chat message to send" }
303 platform: #{ type: "string"
304 description: "Platform: youtube or twitch (default: twitch)"
305 enum: #["youtube" "twitch"] } }
306 required: #["message"] }
307 handler: (lambda (params) (tool-chat config params)))
309 (mcp-server-register-tool! server
310 name: "tube/status"
311 description: "Overview of both platforms — subscriber/follower counts, recent activity, live status, and channel info."
312 schema: #{ type: "object"
313 properties: #{
314 platform: #{ type: "string"
315 description: "Platform: youtube, twitch, or both (default: both)"
316 enum: #["youtube" "twitch" "both"] } } }
317 handler: (lambda (params) (tool-status config params)))))
319 ))