@mkterswingman/5mghost-yonder 0.0.26 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/dist/auth/oauthFlow.d.ts +0 -9
- package/dist/auth/oauthFlow.js +0 -151
- package/dist/auth/sharedAuth.d.ts +0 -10
- package/dist/auth/sharedAuth.js +0 -31
- package/dist/auth/tokenManager.d.ts +0 -18
- package/dist/auth/tokenManager.js +0 -92
- package/dist/cli/check.d.ts +0 -4
- package/dist/cli/check.js +0 -90
- package/dist/cli/index.d.ts +0 -18
- package/dist/cli/index.js +0 -166
- package/dist/cli/installSkills.d.ts +0 -1
- package/dist/cli/installSkills.js +0 -39
- package/dist/cli/runtime.d.ts +0 -9
- package/dist/cli/runtime.js +0 -35
- package/dist/cli/serve.d.ts +0 -1
- package/dist/cli/serve.js +0 -26
- package/dist/cli/setup.d.ts +0 -33
- package/dist/cli/setup.js +0 -450
- package/dist/cli/setupCookies.d.ts +0 -88
- package/dist/cli/setupCookies.js +0 -431
- package/dist/cli/smoke.d.ts +0 -27
- package/dist/cli/smoke.js +0 -108
- package/dist/cli/uninstall.d.ts +0 -16
- package/dist/cli/uninstall.js +0 -99
- package/dist/download/downloader.d.ts +0 -67
- package/dist/download/downloader.js +0 -309
- package/dist/download/jobManager.d.ts +0 -22
- package/dist/download/jobManager.js +0 -211
- package/dist/download/types.d.ts +0 -44
- package/dist/download/types.js +0 -1
- package/dist/runtime/ffmpegRuntime.d.ts +0 -13
- package/dist/runtime/ffmpegRuntime.js +0 -51
- package/dist/runtime/installers.d.ts +0 -12
- package/dist/runtime/installers.js +0 -45
- package/dist/runtime/manifest.d.ts +0 -18
- package/dist/runtime/manifest.js +0 -43
- package/dist/runtime/playwrightRuntime.d.ts +0 -17
- package/dist/runtime/playwrightRuntime.js +0 -49
- package/dist/runtime/systemDeps.d.ts +0 -3
- package/dist/runtime/systemDeps.js +0 -30
- package/dist/runtime/ytdlpRuntime.d.ts +0 -14
- package/dist/runtime/ytdlpRuntime.js +0 -58
- package/dist/server.d.ts +0 -23
- package/dist/server.js +0 -81
- package/dist/tools/downloads.d.ts +0 -11
- package/dist/tools/downloads.js +0 -220
- package/dist/tools/remote.d.ts +0 -4
- package/dist/tools/remote.js +0 -239
- package/dist/tools/subtitles.d.ts +0 -29
- package/dist/tools/subtitles.js +0 -713
- package/dist/utils/browserLaunch.d.ts +0 -5
- package/dist/utils/browserLaunch.js +0 -22
- package/dist/utils/browserProfileImport.d.ts +0 -49
- package/dist/utils/browserProfileImport.js +0 -163
- package/dist/utils/codexInternal.d.ts +0 -9
- package/dist/utils/codexInternal.js +0 -60
- package/dist/utils/config.d.ts +0 -53
- package/dist/utils/config.js +0 -77
- package/dist/utils/cookieRefresh.d.ts +0 -18
- package/dist/utils/cookieRefresh.js +0 -70
- package/dist/utils/cookies.d.ts +0 -18
- package/dist/utils/cookies.js +0 -100
- package/dist/utils/ffmpeg.d.ts +0 -5
- package/dist/utils/ffmpeg.js +0 -16
- package/dist/utils/ffmpegPath.d.ts +0 -8
- package/dist/utils/ffmpegPath.js +0 -21
- package/dist/utils/formatters.d.ts +0 -4
- package/dist/utils/formatters.js +0 -42
- package/dist/utils/launcher.d.ts +0 -12
- package/dist/utils/launcher.js +0 -90
- package/dist/utils/mcpRegistration.d.ts +0 -7
- package/dist/utils/mcpRegistration.js +0 -23
- package/dist/utils/mediaPaths.d.ts +0 -7
- package/dist/utils/mediaPaths.js +0 -10
- package/dist/utils/openClaw.d.ts +0 -18
- package/dist/utils/openClaw.js +0 -82
- package/dist/utils/skills.d.ts +0 -16
- package/dist/utils/skills.js +0 -56
- package/dist/utils/videoInput.d.ts +0 -5
- package/dist/utils/videoInput.js +0 -58
- package/dist/utils/videoMetadata.d.ts +0 -11
- package/dist/utils/videoMetadata.js +0 -1
- package/dist/utils/ytdlp.d.ts +0 -28
- package/dist/utils/ytdlp.js +0 -205
- package/dist/utils/ytdlpFailures.d.ts +0 -3
- package/dist/utils/ytdlpFailures.js +0 -27
- package/dist/utils/ytdlpPath.d.ts +0 -33
- package/dist/utils/ytdlpPath.js +0 -77
- package/dist/utils/ytdlpProgress.d.ts +0 -13
- package/dist/utils/ytdlpProgress.js +0 -77
- package/dist/utils/ytdlpScheduler.d.ts +0 -25
- package/dist/utils/ytdlpScheduler.js +0 -78
package/dist/tools/downloads.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { MEDIA_ROOT_DIR } from "../utils/config.js";
|
|
3
|
-
import { downloadOneItem } from "../download/downloader.js";
|
|
4
|
-
import { buildMediaOutputPaths } from "../utils/mediaPaths.js";
|
|
5
|
-
import { resolveVideoInput } from "../utils/videoInput.js";
|
|
6
|
-
const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkterswingman/5mghost-yonder setup\n• PAT: 设置环境变量 YT_MCP_TOKEN 或在 https://mkterswingman.com/pat/login 生成 token";
|
|
7
|
-
const DOWNLOAD_MODES = ["video", "subtitles", "both"];
|
|
8
|
-
const VIDEO_QUALITIES = ["1080p", "max"];
|
|
9
|
-
const SUBTITLE_FORMATS = ["vtt", "srt", "ttml", "srv3", "csv"];
|
|
10
|
-
const MAX_DOWNLOAD_VIDEOS = 5;
|
|
11
|
-
class DownloadToolError extends Error {
|
|
12
|
-
code;
|
|
13
|
-
constructor(code, message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.code = code;
|
|
16
|
-
this.name = "DownloadToolError";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function toolOk(payload) {
|
|
20
|
-
return {
|
|
21
|
-
structuredContent: payload,
|
|
22
|
-
content: [
|
|
23
|
-
{
|
|
24
|
-
type: "text",
|
|
25
|
-
text: JSON.stringify(payload),
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function toolErr(code, message) {
|
|
31
|
-
const payload = { status: "failed", error: { code, message } };
|
|
32
|
-
return {
|
|
33
|
-
structuredContent: payload,
|
|
34
|
-
isError: true,
|
|
35
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function todayDateStr() {
|
|
39
|
-
const d = new Date();
|
|
40
|
-
const yyyy = d.getFullYear();
|
|
41
|
-
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
42
|
-
const dd = String(d.getDate()).padStart(2, "0");
|
|
43
|
-
return `${yyyy}-${mm}-${dd}`;
|
|
44
|
-
}
|
|
45
|
-
function dedupeValues(values) {
|
|
46
|
-
return values == null ? undefined : [...new Set(values)];
|
|
47
|
-
}
|
|
48
|
-
function toSourceUrl(input, videoId) {
|
|
49
|
-
const trimmed = input.trim();
|
|
50
|
-
if (/^https?:\/\//u.test(trimmed)) {
|
|
51
|
-
return trimmed;
|
|
52
|
-
}
|
|
53
|
-
return `https://www.youtube.com/watch?v=${videoId}`;
|
|
54
|
-
}
|
|
55
|
-
function normalizeSubtitleFormats(mode, subtitleFormats) {
|
|
56
|
-
if (mode === "video") {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
const normalized = dedupeValues(subtitleFormats) ?? ["srt"];
|
|
60
|
-
if (normalized.length === 0) {
|
|
61
|
-
throw new DownloadToolError("INVALID_INPUT", "subtitle_formats must include at least one format when mode includes subtitles");
|
|
62
|
-
}
|
|
63
|
-
return normalized;
|
|
64
|
-
}
|
|
65
|
-
function buildJobInput(videos, mode, videoQuality, subtitleFormats, deps) {
|
|
66
|
-
const invalidInputs = [];
|
|
67
|
-
const resolvedVideos = [];
|
|
68
|
-
const seenIds = new Set();
|
|
69
|
-
const dateStr = deps.nowDate?.() ?? todayDateStr();
|
|
70
|
-
const mediaRootDir = deps.mediaRootDir ?? MEDIA_ROOT_DIR;
|
|
71
|
-
for (const rawInput of videos) {
|
|
72
|
-
const videoId = resolveVideoInput(rawInput);
|
|
73
|
-
if (!videoId) {
|
|
74
|
-
invalidInputs.push(rawInput);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (seenIds.has(videoId)) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
seenIds.add(videoId);
|
|
81
|
-
resolvedVideos.push({
|
|
82
|
-
video_id: videoId,
|
|
83
|
-
// Why: yt-dlp should keep the original URL when the user passed one so polls reflect the submitted source.
|
|
84
|
-
source_url: toSourceUrl(rawInput, videoId),
|
|
85
|
-
title: videoId,
|
|
86
|
-
output_dir: buildMediaOutputPaths(mediaRootDir, dateStr, videoId).outputDir,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
if (resolvedVideos.length === 0) {
|
|
90
|
-
throw new DownloadToolError("INVALID_INPUT", `无法解析任何视频 ID。无效输入: ${invalidInputs.join(", ")}`);
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
jobInput: {
|
|
94
|
-
videos: resolvedVideos,
|
|
95
|
-
mode,
|
|
96
|
-
video_quality: mode === "subtitles" ? undefined : videoQuality,
|
|
97
|
-
subtitle_formats: normalizeSubtitleFormats(mode, subtitleFormats),
|
|
98
|
-
},
|
|
99
|
-
invalidInputs,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
async function runDownloadJob(manager, snapshot, subtitleLanguages, defaultSubtitleLanguages, deps) {
|
|
103
|
-
const executeItem = deps.executeItem ?? downloadOneItem;
|
|
104
|
-
for (const item of snapshot.items) {
|
|
105
|
-
try {
|
|
106
|
-
await executeItem({
|
|
107
|
-
jobManager: manager,
|
|
108
|
-
jobId: snapshot.job_id,
|
|
109
|
-
item,
|
|
110
|
-
mode: snapshot.mode,
|
|
111
|
-
videoQuality: snapshot.video_quality,
|
|
112
|
-
subtitleFormats: snapshot.subtitle_formats,
|
|
113
|
-
subtitleLanguages,
|
|
114
|
-
defaultSubtitleLanguages,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// downloadOneItem already records per-item failures in the manager.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const latest = manager.pollJob(snapshot.job_id);
|
|
122
|
-
if (latest.status !== "completed" && latest.status !== "failed") {
|
|
123
|
-
const stalledItems = latest.items.filter((item) => item.status === "queued");
|
|
124
|
-
for (const item of stalledItems) {
|
|
125
|
-
manager.failItem(snapshot.job_id, item.video_id, "Download job stopped before this item started");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
export function registerDownloadTools(server, config, tokenManager, manager, deps = {}) {
|
|
130
|
-
const pendingJobs = [];
|
|
131
|
-
let draining = false;
|
|
132
|
-
async function requireAuth() {
|
|
133
|
-
const token = await tokenManager.getValidToken();
|
|
134
|
-
if (!token)
|
|
135
|
-
return AUTH_REQUIRED_MSG;
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
async function drainQueue() {
|
|
139
|
-
if (draining) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
draining = true;
|
|
143
|
-
try {
|
|
144
|
-
while (pendingJobs.length > 0) {
|
|
145
|
-
const nextJob = pendingJobs.shift();
|
|
146
|
-
if (!nextJob) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
await runDownloadJob(manager, nextJob.snapshot, nextJob.subtitleLanguages, nextJob.defaultSubtitleLanguages, deps);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
draining = false;
|
|
154
|
-
if (pendingJobs.length > 0) {
|
|
155
|
-
void drainQueue();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function enqueueDownloadJob(snapshot, subtitleLanguages, defaultSubtitleLanguages) {
|
|
160
|
-
pendingJobs.push({
|
|
161
|
-
snapshot,
|
|
162
|
-
subtitleLanguages,
|
|
163
|
-
defaultSubtitleLanguages,
|
|
164
|
-
});
|
|
165
|
-
void drainQueue();
|
|
166
|
-
}
|
|
167
|
-
server.registerTool("start_download_job", {
|
|
168
|
-
description: "Start a local media download job for up to 5 YouTube videos. Supports video, subtitles, or both and returns a job snapshot for polling.",
|
|
169
|
-
inputSchema: {
|
|
170
|
-
videos: z.array(z.string().min(1)).min(1).max(MAX_DOWNLOAD_VIDEOS),
|
|
171
|
-
mode: z.enum(DOWNLOAD_MODES).optional(),
|
|
172
|
-
video_quality: z.enum(VIDEO_QUALITIES).optional(),
|
|
173
|
-
subtitle_formats: z.array(z.enum(SUBTITLE_FORMATS)).min(1).optional(),
|
|
174
|
-
subtitle_languages: z.array(z.string().min(1)).min(1).optional(),
|
|
175
|
-
},
|
|
176
|
-
}, async ({ videos, mode, video_quality, subtitle_formats, subtitle_languages }) => {
|
|
177
|
-
const authErr = await requireAuth();
|
|
178
|
-
if (authErr)
|
|
179
|
-
return toolErr("AUTH_REQUIRED", authErr);
|
|
180
|
-
if (!Array.isArray(videos) || videos.length === 0) {
|
|
181
|
-
return toolErr("INVALID_INPUT", "videos must include at least one input");
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const resolvedMode = mode ?? "both";
|
|
185
|
-
const { jobInput, invalidInputs } = buildJobInput(videos, resolvedMode, video_quality, subtitle_formats, deps);
|
|
186
|
-
const snapshot = manager.createJob(jobInput);
|
|
187
|
-
enqueueDownloadJob(snapshot, dedupeValues(subtitle_languages), resolvedMode === "video" ? undefined : [...config.default_languages]);
|
|
188
|
-
const queuedSnapshot = manager.pollJob(snapshot.job_id);
|
|
189
|
-
return toolOk({
|
|
190
|
-
...queuedSnapshot,
|
|
191
|
-
invalid_inputs: invalidInputs.length > 0 ? invalidInputs : undefined,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
if (error instanceof DownloadToolError) {
|
|
196
|
-
return toolErr(error.code, error.message);
|
|
197
|
-
}
|
|
198
|
-
return toolErr("INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
server.registerTool("poll_download_job", {
|
|
202
|
-
description: "Poll a local media download job and return the current whole-job snapshot.",
|
|
203
|
-
inputSchema: {
|
|
204
|
-
job_id: z.string().min(1),
|
|
205
|
-
},
|
|
206
|
-
}, async ({ job_id }) => {
|
|
207
|
-
const authErr = await requireAuth();
|
|
208
|
-
if (authErr)
|
|
209
|
-
return toolErr("AUTH_REQUIRED", authErr);
|
|
210
|
-
if (!job_id) {
|
|
211
|
-
return toolErr("INVALID_INPUT", "job_id is required");
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
return toolOk(manager.pollJob(job_id));
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
return toolErr("JOB_NOT_FOUND", error instanceof Error ? error.message : String(error));
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
package/dist/tools/remote.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import type { YtMcpConfig } from "../utils/config.js";
|
|
3
|
-
import type { TokenManager } from "../auth/tokenManager.js";
|
|
4
|
-
export declare function registerRemoteTools(server: McpServer, config: YtMcpConfig, tokenManager: TokenManager): void;
|
package/dist/tools/remote.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
function toolErr(code, message) {
|
|
3
|
-
const payload = { status: "failed", error: { code, message } };
|
|
4
|
-
return {
|
|
5
|
-
structuredContent: payload,
|
|
6
|
-
isError: true,
|
|
7
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
function createRemoteTool(server, def, tokenManager, apiUrl) {
|
|
11
|
-
server.registerTool(def.name, {
|
|
12
|
-
description: def.description,
|
|
13
|
-
inputSchema: def.schema,
|
|
14
|
-
}, async (params) => {
|
|
15
|
-
const token = await tokenManager.getValidToken();
|
|
16
|
-
if (!token) {
|
|
17
|
-
return toolErr("AUTH_EXPIRED", "请重新运行 npx @mkterswingman/5mghost-yonder setup");
|
|
18
|
-
}
|
|
19
|
-
let res;
|
|
20
|
-
try {
|
|
21
|
-
const method = def.method ?? "POST";
|
|
22
|
-
const url = new URL(`${apiUrl}/${def.remotePath}`);
|
|
23
|
-
const headers = {
|
|
24
|
-
Authorization: `Bearer ${token}`,
|
|
25
|
-
};
|
|
26
|
-
const fetchOptions = { method, headers };
|
|
27
|
-
if (method === "GET") {
|
|
28
|
-
// For GET requests, pass params as query string
|
|
29
|
-
for (const [key, value] of Object.entries(params)) {
|
|
30
|
-
if (value !== undefined && value !== null) {
|
|
31
|
-
url.searchParams.set(key, String(value));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
headers["Content-Type"] = "application/json";
|
|
37
|
-
fetchOptions.body = JSON.stringify(params);
|
|
38
|
-
}
|
|
39
|
-
res = await fetch(url.toString(), fetchOptions);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
return toolErr("REMOTE_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
-
}
|
|
44
|
-
if (res.status === 401) {
|
|
45
|
-
return toolErr("AUTH_EXPIRED", "Token 已过期");
|
|
46
|
-
}
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
const text = await res.text().catch(() => "");
|
|
49
|
-
return toolErr("REMOTE_ERROR", `服务端错误 ${res.status}: ${text.slice(0, 200)}`);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const body = (await res.json());
|
|
53
|
-
// If the remote already returns MCP-shaped content, pass through
|
|
54
|
-
if (body.content && Array.isArray(body.content)) {
|
|
55
|
-
return body;
|
|
56
|
-
}
|
|
57
|
-
// Otherwise wrap it
|
|
58
|
-
return {
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: "text",
|
|
62
|
-
text: JSON.stringify(body),
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return toolErr("REMOTE_ERROR", "服务端返回了非 JSON 内容");
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
const REMOTE_TOOLS = [
|
|
73
|
-
{
|
|
74
|
-
name: "search_videos",
|
|
75
|
-
description: "Search YouTube videos by keyword. Supports up to 300 results.",
|
|
76
|
-
schema: {
|
|
77
|
-
query: z.string().min(1),
|
|
78
|
-
max_results: z.number().int().min(1).max(300).optional(),
|
|
79
|
-
order: z
|
|
80
|
-
.enum([
|
|
81
|
-
"date",
|
|
82
|
-
"rating",
|
|
83
|
-
"relevance",
|
|
84
|
-
"title",
|
|
85
|
-
"videoCount",
|
|
86
|
-
"viewCount",
|
|
87
|
-
])
|
|
88
|
-
.optional(),
|
|
89
|
-
},
|
|
90
|
-
remotePath: "api/search",
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: "get_video_stats",
|
|
94
|
-
description: "Get YouTube video stats in one request. Accepts up to 1000 video IDs/URLs.",
|
|
95
|
-
schema: {
|
|
96
|
-
videos: z.array(z.string().min(1)).min(1).max(1000),
|
|
97
|
-
},
|
|
98
|
-
remotePath: "api/video-stats/bulk",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: "start_video_stats_job",
|
|
102
|
-
description: "Start an async YouTube video stats job. Accepts up to 1000 items.",
|
|
103
|
-
schema: {
|
|
104
|
-
videos: z.array(z.string().min(1)).min(1).max(1000),
|
|
105
|
-
},
|
|
106
|
-
remotePath: "api/video-stats/job/start",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: "poll_video_stats_job",
|
|
110
|
-
description: "Poll job progress and get partial/final results for a job_id.",
|
|
111
|
-
schema: {
|
|
112
|
-
job_id: z.string().min(1),
|
|
113
|
-
},
|
|
114
|
-
remotePath: "api/video-stats/job/poll",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "resume_video_stats_job",
|
|
118
|
-
description: "Resume a previously partial job with a resume_token.",
|
|
119
|
-
schema: {
|
|
120
|
-
resume_token: z.string().min(1),
|
|
121
|
-
},
|
|
122
|
-
remotePath: "api/video-stats/job/resume",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: "get_trending",
|
|
126
|
-
description: "Get official YouTube mostPopular videos. Supports region/category filters.",
|
|
127
|
-
schema: {
|
|
128
|
-
region_code: z
|
|
129
|
-
.string()
|
|
130
|
-
.regex(/^[A-Za-z]{2}$/)
|
|
131
|
-
.optional(),
|
|
132
|
-
category_id: z.string().min(1).max(64).optional(),
|
|
133
|
-
max_results: z.number().int().min(1).max(300).optional(),
|
|
134
|
-
},
|
|
135
|
-
remotePath: "api/trending",
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: "search_channels",
|
|
139
|
-
description: "Search YouTube channels by keyword for candidate discovery.",
|
|
140
|
-
schema: {
|
|
141
|
-
query: z.string().min(1).max(200),
|
|
142
|
-
max_results: z.number().int().min(1).max(50).optional(),
|
|
143
|
-
},
|
|
144
|
-
remotePath: "api/channel/search",
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: "get_channel_stats",
|
|
148
|
-
description: "Get YouTube channel statistics for up to 50 channel inputs.",
|
|
149
|
-
schema: {
|
|
150
|
-
channels: z.array(z.string().min(1)).min(1).max(50).optional(),
|
|
151
|
-
channel_ids: z.array(z.string().min(1)).min(1).max(50).optional(),
|
|
152
|
-
},
|
|
153
|
-
remotePath: "api/channel/stats",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "list_channel_uploads",
|
|
157
|
-
description: "List videos uploaded by a YouTube channel. Supports up to 1000 results.",
|
|
158
|
-
schema: {
|
|
159
|
-
channel: z.string().min(1).optional(),
|
|
160
|
-
channel_id: z.string().min(1).optional(),
|
|
161
|
-
max_results: z.number().int().min(1).max(1000).optional(),
|
|
162
|
-
},
|
|
163
|
-
remotePath: "api/channel/uploads",
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: "get_comments",
|
|
167
|
-
description: "Get comments for one YouTube video. max_comments up to 1000.",
|
|
168
|
-
schema: {
|
|
169
|
-
video: z.string().min(1),
|
|
170
|
-
max_comments: z.number().int().min(1).max(1000).optional(),
|
|
171
|
-
order: z.enum(["time", "relevance"]).optional(),
|
|
172
|
-
include_replies_preview: z.boolean().optional(),
|
|
173
|
-
replies_preview_per_comment: z.number().int().min(1).max(5).optional(),
|
|
174
|
-
replies_preview_parent_limit: z.number().int().min(1).max(25).optional(),
|
|
175
|
-
},
|
|
176
|
-
remotePath: "api/comments/sync",
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
name: "start_comments_job",
|
|
180
|
-
description: "Start a comments export job for one YouTube video.",
|
|
181
|
-
schema: {
|
|
182
|
-
video: z.string().min(1),
|
|
183
|
-
max_comments: z.number().int().min(1).max(1000).optional(),
|
|
184
|
-
order: z.enum(["time", "relevance"]).optional(),
|
|
185
|
-
},
|
|
186
|
-
remotePath: "api/comments/start",
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: "poll_comments_job",
|
|
190
|
-
description: "Check comments export job progress by job_id.",
|
|
191
|
-
schema: {
|
|
192
|
-
job_id: z.string().min(1),
|
|
193
|
-
},
|
|
194
|
-
remotePath: "api/comments/poll",
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: "resume_comments_job",
|
|
198
|
-
description: "Resume an unfinished comments export job.",
|
|
199
|
-
schema: {
|
|
200
|
-
resume_token: z.string().min(1),
|
|
201
|
-
},
|
|
202
|
-
remotePath: "api/comments/resume",
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: "get_comment_replies",
|
|
206
|
-
description: "Get 2nd-level replies for one top-level comment.",
|
|
207
|
-
schema: {
|
|
208
|
-
parent_comment_id: z.string().min(1),
|
|
209
|
-
max_results: z.number().int().min(1).max(1000).optional(),
|
|
210
|
-
page_token: z.string().min(1).optional(),
|
|
211
|
-
},
|
|
212
|
-
remotePath: "api/comments/replies",
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
name: "get_quota_usage",
|
|
216
|
-
description: "Get YouTube API quota usage for today or a specific date.",
|
|
217
|
-
schema: {
|
|
218
|
-
date: z
|
|
219
|
-
.string()
|
|
220
|
-
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
221
|
-
.optional(),
|
|
222
|
-
},
|
|
223
|
-
remotePath: "api/quota",
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
name: "get_patch_notes",
|
|
227
|
-
description: "Read server patch notes for latest MCP/API updates.",
|
|
228
|
-
schema: {
|
|
229
|
-
max_chars: z.number().int().min(500).max(50_000).optional(),
|
|
230
|
-
},
|
|
231
|
-
remotePath: "api/patch-notes",
|
|
232
|
-
method: "GET",
|
|
233
|
-
},
|
|
234
|
-
];
|
|
235
|
-
export function registerRemoteTools(server, config, tokenManager) {
|
|
236
|
-
for (const def of REMOTE_TOOLS) {
|
|
237
|
-
createRemoteTool(server, def, tokenManager, config.api_url);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import type { YtMcpConfig } from "../utils/config.js";
|
|
3
|
-
import type { TokenManager } from "../auth/tokenManager.js";
|
|
4
|
-
export declare function toReadableSubtitleJobError(error: unknown): string;
|
|
5
|
-
/**
|
|
6
|
-
* Convert VTT subtitle content to clean, human-readable CSV.
|
|
7
|
-
*
|
|
8
|
-
* YouTube auto-captions use a "rolling" VTT format where each cue has two
|
|
9
|
-
* lines: the first line repeats the previous cue's text, and the second line
|
|
10
|
-
* contains new words (marked with <c> tags for word-level timing). This
|
|
11
|
-
* function detects and handles this pattern:
|
|
12
|
-
*
|
|
13
|
-
* 1. Detects auto-caption format (presence of <c> word-timing tags)
|
|
14
|
-
* 2. For auto-captions: extracts only the NEW text from each cue's second
|
|
15
|
-
* line, skips transition cues, and concatenates into clean sentences
|
|
16
|
-
* 3. For manual subtitles: passes through cleanly with no data loss
|
|
17
|
-
* 4. Outputs: start_time, end_time, text
|
|
18
|
-
*/
|
|
19
|
-
export declare function vttToCsv(vtt: string): string;
|
|
20
|
-
export declare function downloadSubtitlesForLanguages(input: {
|
|
21
|
-
videoId: string;
|
|
22
|
-
sourceUrl?: string;
|
|
23
|
-
languages: string[];
|
|
24
|
-
formats: string[];
|
|
25
|
-
subtitlesDir: string;
|
|
26
|
-
skipMissingLanguages?: boolean;
|
|
27
|
-
onProgress?: (completed: number, total: number) => void;
|
|
28
|
-
}): Promise<Record<string, string[]>>;
|
|
29
|
-
export declare function registerSubtitleTools(server: McpServer, config: YtMcpConfig, tokenManager: TokenManager): void;
|