AtlatestRepositorytube
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.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-date26
default-end-date27
default-now-timestamp28
tool-analytics29
tool-schedule30
tool-go-live31
tool-chat32
tool-status)33
(begin35
;;; Compute default start date (30 days ago).36
;;; Must be called outside async contexts due to VM yield bug37
;;; 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 bug43
;;; 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 pair59
;;; of (platform-name . thunk). Returns results from enabled platforms60
;;; that succeed, or the fallback message if none produced data.61
(define (collect-platform-results config requested providers fallback)62
(let ((results63
(filter identity64
(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
fallback74
(string-join results "\n\n"))))76
;; ============================================================77
;; Tool Handlers78
;; ============================================================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 file86
(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-append102
"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-default106
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 days112
(format-date (- now-timestamp113
(* days 86400)))114
start-default)))115
(end-date (dict-ref params end_date: end-default)))116
(collect-platform-results config platform117
(list118
(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-append125
"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 config131
(lambda (client broadcaster-id)132
(let* ((followers (twitch-followers client broadcaster-id133
first: 1))134
(total (dict-ref followers total: 0))135
(clips (twitch-clips client136
broadcaster-id: broadcaster-id137
first: 5)))138
(string-append139
"Twitch Analytics:\n"140
"Total Followers: " (number->string total) "\n"141
(if (null? clips)142
"No recent clips."143
(string-append144
"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 platform153
(list154
(cons "youtube"155
(lambda ()156
(let ((broadcasts (tube-list-broadcasts config "upcoming")))157
(if (null? broadcasts)158
"YouTube: No upcoming broadcasts."159
(string-append160
"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-append168
"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-second177
;; inside async context (VM yield bug)178
(today (if (pair? rest) (car rest) (default-end-date))))179
(collect-platform-results config platform180
(list181
(cons "youtube"182
(lambda ()183
(let* ((scheduled (string-append today "T00:00:00Z"))184
(broadcast (tube-create-broadcast config title scheduled)))185
(string-append186
"YouTube broadcast created: "187
(youtube-broadcast-id broadcast)))))188
(cons "twitch"189
(lambda ()190
(with-twitch-context config191
(lambda (client broadcaster-id)192
(twitch-modify-channel client broadcaster-id193
#{ 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 message201
(error "message parameter is required"))202
(cond203
((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
(else209
(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 platform215
(list216
(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 Registration222
;; ============================================================224
;;; Register all tube MCP tools on the given server.225
;;; Precomputes default dates here (before async context) to avoid226
;;; 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! server233
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! server250
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! server259
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! server275
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! server285
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! server297
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! server310
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
))