@mkterswingman/5mghost-yonder 0.0.38 → 0.0.39

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/dist/cli/index.js CHANGED
@@ -28,6 +28,7 @@ Commands:
28
28
  setup-cookies Refresh YouTube cookies using browser login
29
29
  runtime Manage required runtimes
30
30
  check Check auth, runtime, cookies, and connectivity
31
+ doctor Alias for check; diagnose local install and runtime health
31
32
  uninstall Remove MCP registrations and local yt-mcp config
32
33
  update Update the main package and required runtimes
33
34
  version Show current version
@@ -70,6 +71,13 @@ export async function runUnifiedUpdate(deps) {
70
71
  const command = process.argv[2];
71
72
  async function main() {
72
73
  switch (command) {
74
+ case undefined:
75
+ case "help":
76
+ case "--help":
77
+ case "-h": {
78
+ console.log(buildHelpText(getVersion()));
79
+ break;
80
+ }
73
81
  case "setup": {
74
82
  const { runSetup } = await import("./setup.js");
75
83
  await runSetup();
@@ -118,6 +126,11 @@ async function main() {
118
126
  await runCheck();
119
127
  break;
120
128
  }
129
+ case "doctor": {
130
+ const { runCheck } = await import("./check.js");
131
+ await runCheck();
132
+ break;
133
+ }
121
134
  case "update": {
122
135
  const current = getVersion();
123
136
  console.log(`Current version: ${current}`);
@@ -165,7 +178,7 @@ async function main() {
165
178
  }
166
179
  default:
167
180
  console.log(buildHelpText(getVersion()));
168
- process.exit(command ? 1 : 0);
181
+ process.exit(1);
169
182
  }
170
183
  }
171
184
  main().catch((err) => {
@@ -2,6 +2,8 @@ import { fileURLToPath } from "node:url";
2
2
  import { dirname, join } from "node:path";
3
3
  import { buildSkillInstallPlan, installSkillTarget, } from "@mkterswingman/5mghost-shared-client/registration";
4
4
  import { detectCli } from "@mkterswingman/5mghost-shared-client";
5
+ import { getWorkBuddySkillsDir, isWorkBuddyInstallLikelyInstalled } from "../utils/workBuddy.js";
6
+ import { getCodeBuddySkillsDir, isCodeBuddyInstallLikelyInstalled } from "../utils/codeBuddy.js";
5
7
  function resolvePackageRoot() {
6
8
  return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
7
9
  }
@@ -19,6 +21,22 @@ export async function runInstallSkills() {
19
21
  availableCliNames,
20
22
  includeOpenClaw: process.env.YT_MCP_INSTALL_OPENCLAW_SKILL === "1",
21
23
  });
24
+ if (isWorkBuddyInstallLikelyInstalled() && !plan.some((target) => target.client === "workbuddy")) {
25
+ plan.push({
26
+ client: "workbuddy",
27
+ label: "WorkBuddy",
28
+ sourceDir: join(packageRoot, "skills", "use-yt-mcp"),
29
+ targetDir: join(getWorkBuddySkillsDir(), "use-yt-mcp"),
30
+ });
31
+ }
32
+ if (isCodeBuddyInstallLikelyInstalled() && !plan.some((target) => target.client === "codebuddy")) {
33
+ plan.push({
34
+ client: "codebuddy",
35
+ label: "CodeBuddy",
36
+ sourceDir: join(packageRoot, "skills", "use-yt-mcp"),
37
+ targetDir: join(getCodeBuddySkillsDir(), "use-yt-mcp"),
38
+ });
39
+ }
22
40
  if (plan.length === 0) {
23
41
  console.log("[yt-mcp] No supported AI client detected for bundled skill install.");
24
42
  return;
package/dist/cli/setup.js CHANGED
@@ -7,6 +7,8 @@ import { runOAuthFlow } from "@mkterswingman/5mghost-shared-client/auth";
7
7
  import { hasSIDCookies } from "../utils/cookies.js";
8
8
  import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
9
9
  import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
10
+ import { getWorkBuddyConfigPath, isWorkBuddyInstallLikelyInstalled, writeWorkBuddyConfig, } from "../utils/workBuddy.js";
11
+ import { getCodeBuddyConfigPath, isCodeBuddyInstallLikelyInstalled, writeCodeBuddyConfig, } from "../utils/codeBuddy.js";
10
12
  import { checkAll } from "../runtime/installers.js";
11
13
  import { runInstallSkills } from "./installSkills.js";
12
14
  import { registerCliCandidates, registerOpenClaw, } from "@mkterswingman/5mghost-shared-client/registration";
@@ -400,6 +402,28 @@ export async function runSetup() {
400
402
  console.log(` ⚠️ Codex CLI (internal) auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
401
403
  }
402
404
  }
405
+ if (isWorkBuddyInstallLikelyInstalled(getWorkBuddyConfigPath())) {
406
+ try {
407
+ const status = writeWorkBuddyConfig("yt-mcp", launcherCommand);
408
+ const suffix = status === "created" ? "created" : "updated";
409
+ console.log(` ✅ MCP registered in WorkBuddy (${suffix} ${getWorkBuddyConfigPath()})`);
410
+ registered = true;
411
+ }
412
+ catch (err) {
413
+ console.log(` ⚠️ WorkBuddy auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
414
+ }
415
+ }
416
+ if (isCodeBuddyInstallLikelyInstalled(getCodeBuddyConfigPath())) {
417
+ try {
418
+ const status = writeCodeBuddyConfig("yt-mcp", launcherCommand);
419
+ const suffix = status === "created" ? "created" : "updated";
420
+ console.log(` ✅ MCP registered in CodeBuddy (${suffix} ${getCodeBuddyConfigPath()})`);
421
+ registered = true;
422
+ }
423
+ catch (err) {
424
+ console.log(` ⚠️ CodeBuddy auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
425
+ }
426
+ }
403
427
  let skillsInstalled = false;
404
428
  try {
405
429
  process.env.YT_MCP_INSTALL_OPENCLAW_SKILL = isOpenClawInstallLikelyInstalled(detectCli) ? "1" : "0";
@@ -441,6 +465,8 @@ export async function runSetup() {
441
465
  console.log(" OpenClaw stdio CLI (recommended):");
442
466
  console.log(` mcporter config add yt-mcp --command node --arg ${JSON.stringify(PATHS.launcherJs)} --arg serve`);
443
467
  console.log(` OpenClaw uses ${PATHS.sharedAuthJson} for PAT/JWT, so env.YT_MCP_TOKEN is optional after setup.`);
468
+ console.log(` WorkBuddy MCP config: ${getWorkBuddyConfigPath()}`);
469
+ console.log(` CodeBuddy MCP config: ${getCodeBuddyConfigPath()}`);
444
470
  if (skillsInstalled) {
445
471
  console.log(" ✅ Installed bundled yt-mcp analysis skill for detected AI clients");
446
472
  }
@@ -76,9 +76,6 @@ export declare function readCdpCookiesUntilSession(client: CdpCookieClient, deps
76
76
  export declare function tryImportBrowserCookies(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<boolean>;
77
77
  export declare function readImportedBrowserCookies(candidate: BrowserProfileCandidate, chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<PlaywrightCookie[] | null>;
78
78
  export declare function runManualCookieSetup(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<void>;
79
- /**
80
- * Interactive cookie setup — opens a visible browser for user to log in.
81
- */
82
- export declare function parseSetupCookiesArgs(argv: string[]): void;
79
+ export declare function parseSetupCookiesArgs(argv: string[], warn?: (message: string) => void): void;
83
80
  export declare function runSetupCookies(overrides?: Partial<SetupCookiesDeps>): Promise<void>;
84
81
  export {};
@@ -412,9 +412,22 @@ export async function runManualCookieSetup(chromium, deps) {
412
412
  /**
413
413
  * Interactive cookie setup — opens a visible browser for user to log in.
414
414
  */
415
- export function parseSetupCookiesArgs(argv) {
415
+ const LEGACY_SETUP_COOKIES_FLAGS = new Set(["--import-only", "--headed"]);
416
+ export function parseSetupCookiesArgs(argv, warn = console.warn) {
417
+ const legacyFlags = [];
416
418
  for (const arg of argv) {
417
- throw new Error(`Unknown option for setup-cookies: ${arg}. setup-cookies accepts no options.`);
419
+ if (LEGACY_SETUP_COOKIES_FLAGS.has(arg)) {
420
+ if (!legacyFlags.includes(arg)) {
421
+ legacyFlags.push(arg);
422
+ }
423
+ continue;
424
+ }
425
+ throw new Error(`Unknown option for setup-cookies: ${arg}. Supported legacy flags: --import-only, --headed.`);
426
+ }
427
+ if (legacyFlags.length > 0) {
428
+ warn(`[DEPRECATED] setup-cookies now runs a single guided flow. ` +
429
+ `Ignoring legacy flags: ${legacyFlags.join(", ")}. ` +
430
+ `Please run: yt-mcp setup-cookies`);
418
431
  }
419
432
  }
420
433
  export async function runSetupCookies(overrides = {}) {
@@ -1,6 +1,8 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { PATHS } from "../utils/config.js";
4
+ import { getWorkBuddyConfigPath, removeWorkBuddyConfig } from "../utils/workBuddy.js";
5
+ import { getCodeBuddyConfigPath, removeCodeBuddyConfig } from "../utils/codeBuddy.js";
4
6
  import { getOpenClawConfigPath, getCodexInternalConfigPath, removeCodexInternalConfig, unregisterOpenClaw, detectCli, } from "@mkterswingman/5mghost-shared-client";
5
7
  import { unregisterCliCandidates } from "@mkterswingman/5mghost-shared-client/registration";
6
8
  function tryRemoveMcp(command, label) {
@@ -72,6 +74,20 @@ export async function runUninstall() {
72
74
  console.log(" ℹ️ Codex CLI (internal) did not have yt-mcp registered");
73
75
  }
74
76
  }
77
+ const workBuddyStatus = removeWorkBuddyConfig("yt-mcp");
78
+ if (workBuddyStatus === "removed") {
79
+ console.log(` ✅ Removed MCP registration from WorkBuddy (${getWorkBuddyConfigPath()})`);
80
+ }
81
+ else {
82
+ console.log(" ℹ️ WorkBuddy did not have yt-mcp registered");
83
+ }
84
+ const codeBuddyStatus = removeCodeBuddyConfig("yt-mcp");
85
+ if (codeBuddyStatus === "removed") {
86
+ console.log(` ✅ Removed MCP registration from CodeBuddy (${getCodeBuddyConfigPath()})`);
87
+ }
88
+ else {
89
+ console.log(" ℹ️ CodeBuddy did not have yt-mcp registered");
90
+ }
75
91
  if (existsSync(PATHS.configDir)) {
76
92
  // Why: ~/.yt-mcp contains launcher, token cache, cookies, and npm cache owned by this package.
77
93
  rmSync(PATHS.configDir, { recursive: true, force: true });
@@ -0,0 +1,17 @@
1
+ export type YouTubeToolHttpMethod = "GET" | "POST";
2
+ export interface YouTubeToolAliasContract {
3
+ name: string;
4
+ remove_after_version: string;
5
+ remove_after_date: string;
6
+ note: string;
7
+ }
8
+ export interface YouTubeToolContract {
9
+ id: string;
10
+ remote_tool_name: string;
11
+ mcp_primary_name: string;
12
+ mcp_aliases: YouTubeToolAliasContract[];
13
+ http_path: string;
14
+ http_method: YouTubeToolHttpMethod;
15
+ }
16
+ export declare const YOUTUBE_TOOL_CONTRACTS: YouTubeToolContract[];
17
+ export declare function getYouTubeToolContractById(id: string): YouTubeToolContract;
@@ -0,0 +1,189 @@
1
+ // AUTO-GENERATED by scripts/sync-youtube-tool-contracts.mjs
2
+ // Source of truth: contracts/youtube-tool-contracts.json
3
+ // Do not edit manually.
4
+ export const YOUTUBE_TOOL_CONTRACTS = [
5
+ {
6
+ "id": "search_videos",
7
+ "remote_tool_name": "search_videos",
8
+ "mcp_primary_name": "search_videos",
9
+ "mcp_aliases": [],
10
+ "http_path": "/api/search",
11
+ "http_method": "POST"
12
+ },
13
+ {
14
+ "id": "get_video_stats",
15
+ "remote_tool_name": "get_video_stats",
16
+ "mcp_primary_name": "get_video_stats",
17
+ "mcp_aliases": [
18
+ {
19
+ "name": "get_video_stats_bulk_sync",
20
+ "remove_after_version": "0.2.0",
21
+ "remove_after_date": "2026-12-31",
22
+ "note": "Legacy name kept for backward compatibility."
23
+ }
24
+ ],
25
+ "http_path": "/api/video-stats/bulk",
26
+ "http_method": "POST"
27
+ },
28
+ {
29
+ "id": "start_video_stats_job",
30
+ "remote_tool_name": "start_video_stats_job",
31
+ "mcp_primary_name": "start_video_stats_job",
32
+ "mcp_aliases": [],
33
+ "http_path": "/api/video-stats/job/start",
34
+ "http_method": "POST"
35
+ },
36
+ {
37
+ "id": "poll_video_stats_job",
38
+ "remote_tool_name": "poll_video_stats_job",
39
+ "mcp_primary_name": "poll_video_stats_job",
40
+ "mcp_aliases": [],
41
+ "http_path": "/api/video-stats/job/poll",
42
+ "http_method": "POST"
43
+ },
44
+ {
45
+ "id": "resume_video_stats_job",
46
+ "remote_tool_name": "resume_video_stats_job",
47
+ "mcp_primary_name": "resume_video_stats_job",
48
+ "mcp_aliases": [],
49
+ "http_path": "/api/video-stats/job/resume",
50
+ "http_method": "POST"
51
+ },
52
+ {
53
+ "id": "get_trending",
54
+ "remote_tool_name": "get_trending",
55
+ "mcp_primary_name": "get_trending",
56
+ "mcp_aliases": [
57
+ {
58
+ "name": "get_trending_videos",
59
+ "remove_after_version": "0.2.0",
60
+ "remove_after_date": "2026-12-31",
61
+ "note": "Legacy name kept for backward compatibility."
62
+ }
63
+ ],
64
+ "http_path": "/api/trending",
65
+ "http_method": "POST"
66
+ },
67
+ {
68
+ "id": "search_channels",
69
+ "remote_tool_name": "search_channels",
70
+ "mcp_primary_name": "search_channels",
71
+ "mcp_aliases": [],
72
+ "http_path": "/api/channel/search",
73
+ "http_method": "POST"
74
+ },
75
+ {
76
+ "id": "get_channel_stats",
77
+ "remote_tool_name": "get_channel_stats",
78
+ "mcp_primary_name": "get_channel_stats",
79
+ "mcp_aliases": [],
80
+ "http_path": "/api/channel/stats",
81
+ "http_method": "POST"
82
+ },
83
+ {
84
+ "id": "list_channel_uploads",
85
+ "remote_tool_name": "list_channel_uploads",
86
+ "mcp_primary_name": "list_channel_uploads",
87
+ "mcp_aliases": [],
88
+ "http_path": "/api/channel/uploads",
89
+ "http_method": "POST"
90
+ },
91
+ {
92
+ "id": "get_comments",
93
+ "remote_tool_name": "get_comments",
94
+ "mcp_primary_name": "get_comments",
95
+ "mcp_aliases": [
96
+ {
97
+ "name": "get_video_comments_sync",
98
+ "remove_after_version": "0.2.0",
99
+ "remove_after_date": "2026-12-31",
100
+ "note": "Legacy name kept for backward compatibility."
101
+ }
102
+ ],
103
+ "http_path": "/api/comments/sync",
104
+ "http_method": "POST"
105
+ },
106
+ {
107
+ "id": "start_comments_job",
108
+ "remote_tool_name": "start_comments_job",
109
+ "mcp_primary_name": "start_comments_job",
110
+ "mcp_aliases": [
111
+ {
112
+ "name": "start_comments_export_job",
113
+ "remove_after_version": "0.2.0",
114
+ "remove_after_date": "2026-12-31",
115
+ "note": "Legacy name kept for backward compatibility."
116
+ }
117
+ ],
118
+ "http_path": "/api/comments/start",
119
+ "http_method": "POST"
120
+ },
121
+ {
122
+ "id": "poll_comments_job",
123
+ "remote_tool_name": "poll_comments_job",
124
+ "mcp_primary_name": "poll_comments_job",
125
+ "mcp_aliases": [
126
+ {
127
+ "name": "poll_comments_export_job",
128
+ "remove_after_version": "0.2.0",
129
+ "remove_after_date": "2026-12-31",
130
+ "note": "Legacy name kept for backward compatibility."
131
+ }
132
+ ],
133
+ "http_path": "/api/comments/poll",
134
+ "http_method": "POST"
135
+ },
136
+ {
137
+ "id": "resume_comments_job",
138
+ "remote_tool_name": "resume_comments_job",
139
+ "mcp_primary_name": "resume_comments_job",
140
+ "mcp_aliases": [
141
+ {
142
+ "name": "resume_comments_export_job",
143
+ "remove_after_version": "0.2.0",
144
+ "remove_after_date": "2026-12-31",
145
+ "note": "Legacy name kept for backward compatibility."
146
+ }
147
+ ],
148
+ "http_path": "/api/comments/resume",
149
+ "http_method": "POST"
150
+ },
151
+ {
152
+ "id": "get_comment_replies",
153
+ "remote_tool_name": "get_comment_replies",
154
+ "mcp_primary_name": "get_comment_replies",
155
+ "mcp_aliases": [
156
+ {
157
+ "name": "get_comment_replies_sync",
158
+ "remove_after_version": "0.2.0",
159
+ "remove_after_date": "2026-12-31",
160
+ "note": "Legacy name kept for backward compatibility."
161
+ }
162
+ ],
163
+ "http_path": "/api/comments/replies",
164
+ "http_method": "POST"
165
+ },
166
+ {
167
+ "id": "get_quota_usage",
168
+ "remote_tool_name": "get_quota_usage",
169
+ "mcp_primary_name": "get_quota_usage",
170
+ "mcp_aliases": [],
171
+ "http_path": "/api/quota",
172
+ "http_method": "POST"
173
+ },
174
+ {
175
+ "id": "get_patch_notes",
176
+ "remote_tool_name": "get_patch_notes",
177
+ "mcp_primary_name": "get_patch_notes",
178
+ "mcp_aliases": [],
179
+ "http_path": "/api/patch-notes",
180
+ "http_method": "GET"
181
+ }
182
+ ];
183
+ export function getYouTubeToolContractById(id) {
184
+ const match = YOUTUBE_TOOL_CONTRACTS.find((tool) => tool.id === id);
185
+ if (!match) {
186
+ throw new Error(`Unknown YouTube tool contract id: ${id}`);
187
+ }
188
+ return match;
189
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { YOUTUBE_TOOL_CONTRACTS } from "../contracts/youtubeToolContracts.js";
2
3
  function toolErr(code, message) {
3
4
  const payload = { status: "failed", error: { code, message } };
4
5
  return {
@@ -69,9 +70,8 @@ function createRemoteTool(server, def, tokenManager, apiUrl) {
69
70
  }
70
71
  });
71
72
  }
72
- const REMOTE_TOOLS = [
73
- {
74
- name: "search_videos",
73
+ const REMOTE_TOOL_META = {
74
+ search_videos: {
75
75
  description: "Search YouTube videos by keyword. Supports up to 300 results.",
76
76
  schema: {
77
77
  query: z.string().min(1),
@@ -87,42 +87,32 @@ const REMOTE_TOOLS = [
87
87
  ])
88
88
  .optional(),
89
89
  },
90
- remotePath: "api/search",
91
90
  },
92
- {
93
- name: "get_video_stats",
91
+ get_video_stats: {
94
92
  description: "Get YouTube video stats in one request. Accepts up to 1000 video IDs/URLs.",
95
93
  schema: {
96
94
  videos: z.array(z.string().min(1)).min(1).max(1000),
97
95
  },
98
- remotePath: "api/video-stats/bulk",
99
96
  },
100
- {
101
- name: "start_video_stats_job",
97
+ start_video_stats_job: {
102
98
  description: "Start an async YouTube video stats job. Accepts up to 1000 items.",
103
99
  schema: {
104
100
  videos: z.array(z.string().min(1)).min(1).max(1000),
105
101
  },
106
- remotePath: "api/video-stats/job/start",
107
102
  },
108
- {
109
- name: "poll_video_stats_job",
103
+ poll_video_stats_job: {
110
104
  description: "Poll job progress and get partial/final results for a job_id.",
111
105
  schema: {
112
106
  job_id: z.string().min(1),
113
107
  },
114
- remotePath: "api/video-stats/job/poll",
115
108
  },
116
- {
117
- name: "resume_video_stats_job",
109
+ resume_video_stats_job: {
118
110
  description: "Resume a previously partial job with a resume_token.",
119
111
  schema: {
120
112
  resume_token: z.string().min(1),
121
113
  },
122
- remotePath: "api/video-stats/job/resume",
123
114
  },
124
- {
125
- name: "get_trending",
115
+ get_trending: {
126
116
  description: "Get official YouTube mostPopular videos. Supports region/category filters.",
127
117
  schema: {
128
118
  region_code: z
@@ -132,38 +122,30 @@ const REMOTE_TOOLS = [
132
122
  category_id: z.string().min(1).max(64).optional(),
133
123
  max_results: z.number().int().min(1).max(300).optional(),
134
124
  },
135
- remotePath: "api/trending",
136
125
  },
137
- {
138
- name: "search_channels",
126
+ search_channels: {
139
127
  description: "Search YouTube channels by keyword for candidate discovery.",
140
128
  schema: {
141
129
  query: z.string().min(1).max(200),
142
130
  max_results: z.number().int().min(1).max(50).optional(),
143
131
  },
144
- remotePath: "api/channel/search",
145
132
  },
146
- {
147
- name: "get_channel_stats",
133
+ get_channel_stats: {
148
134
  description: "Get YouTube channel statistics for up to 50 channel inputs.",
149
135
  schema: {
150
136
  channels: z.array(z.string().min(1)).min(1).max(50).optional(),
151
137
  channel_ids: z.array(z.string().min(1)).min(1).max(50).optional(),
152
138
  },
153
- remotePath: "api/channel/stats",
154
139
  },
155
- {
156
- name: "list_channel_uploads",
140
+ list_channel_uploads: {
157
141
  description: "List videos uploaded by a YouTube channel. Supports up to 1000 results.",
158
142
  schema: {
159
143
  channel: z.string().min(1).optional(),
160
144
  channel_id: z.string().min(1).optional(),
161
145
  max_results: z.number().int().min(1).max(1000).optional(),
162
146
  },
163
- remotePath: "api/channel/uploads",
164
147
  },
165
- {
166
- name: "get_comments",
148
+ get_comments: {
167
149
  description: "Get comments for one YouTube video. max_comments up to 1000.",
168
150
  schema: {
169
151
  video: z.string().min(1),
@@ -173,46 +155,36 @@ const REMOTE_TOOLS = [
173
155
  replies_preview_per_comment: z.number().int().min(1).max(5).optional(),
174
156
  replies_preview_parent_limit: z.number().int().min(1).max(25).optional(),
175
157
  },
176
- remotePath: "api/comments/sync",
177
158
  },
178
- {
179
- name: "start_comments_job",
159
+ start_comments_job: {
180
160
  description: "Start a comments export job for one YouTube video.",
181
161
  schema: {
182
162
  video: z.string().min(1),
183
163
  max_comments: z.number().int().min(1).max(1000).optional(),
184
164
  order: z.enum(["time", "relevance"]).optional(),
185
165
  },
186
- remotePath: "api/comments/start",
187
166
  },
188
- {
189
- name: "poll_comments_job",
167
+ poll_comments_job: {
190
168
  description: "Check comments export job progress by job_id.",
191
169
  schema: {
192
170
  job_id: z.string().min(1),
193
171
  },
194
- remotePath: "api/comments/poll",
195
172
  },
196
- {
197
- name: "resume_comments_job",
173
+ resume_comments_job: {
198
174
  description: "Resume an unfinished comments export job.",
199
175
  schema: {
200
176
  resume_token: z.string().min(1),
201
177
  },
202
- remotePath: "api/comments/resume",
203
178
  },
204
- {
205
- name: "get_comment_replies",
179
+ get_comment_replies: {
206
180
  description: "Get 2nd-level replies for one top-level comment.",
207
181
  schema: {
208
182
  parent_comment_id: z.string().min(1),
209
183
  max_results: z.number().int().min(1).max(1000).optional(),
210
184
  page_token: z.string().min(1).optional(),
211
185
  },
212
- remotePath: "api/comments/replies",
213
186
  },
214
- {
215
- name: "get_quota_usage",
187
+ get_quota_usage: {
216
188
  description: "Get YouTube API quota usage for today or a specific date.",
217
189
  schema: {
218
190
  date: z
@@ -220,18 +192,27 @@ const REMOTE_TOOLS = [
220
192
  .regex(/^\d{4}-\d{2}-\d{2}$/)
221
193
  .optional(),
222
194
  },
223
- remotePath: "api/quota",
224
195
  },
225
- {
226
- name: "get_patch_notes",
196
+ get_patch_notes: {
227
197
  description: "Read server patch notes for latest MCP/API updates.",
228
198
  schema: {
229
199
  max_chars: z.number().int().min(500).max(50_000).optional(),
230
200
  },
231
- remotePath: "api/patch-notes",
232
- method: "GET",
233
201
  },
234
- ];
202
+ };
203
+ const REMOTE_TOOLS = YOUTUBE_TOOL_CONTRACTS.map((contract) => {
204
+ const meta = REMOTE_TOOL_META[contract.id];
205
+ if (!meta) {
206
+ throw new Error(`Missing remote tool metadata for contract id: ${contract.id}`);
207
+ }
208
+ return {
209
+ name: contract.remote_tool_name,
210
+ description: meta.description,
211
+ schema: meta.schema,
212
+ remotePath: contract.http_path.replace(/^\//, ""),
213
+ method: contract.http_method,
214
+ };
215
+ });
235
216
  export function registerRemoteTools(server, config, tokenManager) {
236
217
  for (const def of REMOTE_TOOLS) {
237
218
  createRemoteTool(server, def, tokenManager, config.api_url);
@@ -0,0 +1,22 @@
1
+ export declare const COOKIE_MISSING_MESSAGE = "No valid YouTube cookies found.\nPlease run in your terminal: yt-mcp setup-cookies";
2
+ export declare const COOKIE_EXPIRED_MESSAGE = "YouTube cookies have expired and auto-refresh failed.\nPlease run in your terminal: yt-mcp setup-cookies";
3
+ export declare const COOKIE_INVALID_MESSAGE = "YouTube rejected the current cookies and auto-refresh failed.\nPlease run in your terminal: yt-mcp setup-cookies";
4
+ export declare const SIGN_IN_REQUIRED_MESSAGE = "YouTube requested an additional sign-in confirmation and auto-refresh failed.\nPlease run in your terminal: yt-mcp setup-cookies";
5
+ export declare const RATE_LIMITED_MESSAGE = "The current YouTube session has been rate-limited. Wait and retry later.";
6
+ export declare const COOKIE_JOB_MESSAGE = "YouTube cookies are missing, expired, or invalid.\nPlease run in your terminal: yt-mcp setup-cookies";
7
+ export type CookieFailureCode = "COOKIES_INVALID" | "COOKIES_EXPIRED" | "SIGN_IN_REQUIRED" | "RATE_LIMITED";
8
+ export declare function tryRefreshSubtitleCookies(): Promise<boolean>;
9
+ export declare function isCookieFailureText(error?: string): boolean;
10
+ export declare function classifyYtDlpCookieFailure(stderr: string): {
11
+ code: CookieFailureCode;
12
+ message: string;
13
+ } | null;
14
+ export declare function toReadableSubtitleJobError(error: unknown): string;
15
+ export declare function ensureSubtitleCookiesReady(): Promise<{
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ code: "COOKIES_MISSING" | "COOKIES_EXPIRED";
20
+ toolMessage: string;
21
+ jobMessage: string;
22
+ }>;