@mkterswingman/yt-mcp 0.1.0 → 0.1.2
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/auth/oauthFlow.js +12 -2
- package/dist/tools/remote.js +20 -8
- package/dist/tools/subtitles.js +4 -2
- package/package.json +1 -1
package/dist/auth/oauthFlow.js
CHANGED
|
@@ -9,9 +9,10 @@ function base64url(buf) {
|
|
|
9
9
|
.replace(/=+$/, "");
|
|
10
10
|
}
|
|
11
11
|
export async function runOAuthFlow(authUrl) {
|
|
12
|
-
// 1. Generate PKCE
|
|
12
|
+
// 1. Generate PKCE + state
|
|
13
13
|
const codeVerifier = base64url(randomBytes(32));
|
|
14
14
|
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
15
|
+
const state = base64url(randomBytes(32));
|
|
15
16
|
// 2. Start temp HTTP server to get the actual port for redirect_uri
|
|
16
17
|
const { server: httpServer, port } = await startCallbackServer();
|
|
17
18
|
const redirectUri = `http://127.0.0.1:${port}`;
|
|
@@ -55,6 +56,7 @@ export async function runOAuthFlow(authUrl) {
|
|
|
55
56
|
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
56
57
|
const code = url.searchParams.get("code");
|
|
57
58
|
const error = url.searchParams.get("error");
|
|
59
|
+
const returnedState = url.searchParams.get("state");
|
|
58
60
|
if (error) {
|
|
59
61
|
const safeError = error.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
60
62
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
@@ -68,6 +70,14 @@ export async function runOAuthFlow(authUrl) {
|
|
|
68
70
|
res.end("<h1>Waiting for authorization...</h1>");
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
73
|
+
// Verify state to prevent CSRF
|
|
74
|
+
if (returnedState !== state) {
|
|
75
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
76
|
+
res.end("<h1>Authorization failed</h1><p>State mismatch — possible CSRF attack.</p>");
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(new Error("OAuth state mismatch"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
71
81
|
// Exchange code for tokens
|
|
72
82
|
const tokenBody = {
|
|
73
83
|
grant_type: "authorization_code",
|
|
@@ -111,7 +121,7 @@ export async function runOAuthFlow(authUrl) {
|
|
|
111
121
|
}
|
|
112
122
|
});
|
|
113
123
|
// Open browser
|
|
114
|
-
const authorizeUrl = `${authUrl}/oauth/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${encodeURIComponent(codeChallenge)}&code_challenge_method=S256`;
|
|
124
|
+
const authorizeUrl = `${authUrl}/oauth/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${encodeURIComponent(codeChallenge)}&code_challenge_method=S256&state=${encodeURIComponent(state)}`;
|
|
115
125
|
console.log("\n\x1b[1mOpen this URL in your browser to authorize:\x1b[0m");
|
|
116
126
|
console.log(`\n ${authorizeUrl}\n`);
|
|
117
127
|
import("node:child_process").then(({ exec }) => {
|
package/dist/tools/remote.js
CHANGED
|
@@ -18,14 +18,25 @@ function createRemoteTool(server, def, tokenManager, apiUrl) {
|
|
|
18
18
|
}
|
|
19
19
|
let res;
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
29
40
|
}
|
|
30
41
|
catch (err) {
|
|
31
42
|
return toolErr("REMOTE_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -209,6 +220,7 @@ const REMOTE_TOOLS = [
|
|
|
209
220
|
max_chars: z.number().int().min(500).max(50_000).optional(),
|
|
210
221
|
},
|
|
211
222
|
remotePath: "api/patch-notes",
|
|
223
|
+
method: "GET",
|
|
212
224
|
},
|
|
213
225
|
];
|
|
214
226
|
export function registerRemoteTools(server, config, tokenManager) {
|
package/dist/tools/subtitles.js
CHANGED
|
@@ -95,8 +95,10 @@ async function downloadSubtitle(videoId, lang, format) {
|
|
|
95
95
|
error: result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`,
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
-
// Find the output file - yt-dlp appends lang and format extension
|
|
99
|
-
|
|
98
|
+
// Find the output file - yt-dlp appends lang and format extension.
|
|
99
|
+
// For CSV: yt-dlp downloads as VTT first, so only search for VTT (not stale .csv from previous runs).
|
|
100
|
+
const searchFormat = format === "csv" ? "vtt" : format;
|
|
101
|
+
const possibleExts = [`${lang}.${searchFormat}`, `${lang}.vtt`, `${lang}.srt`, `${lang}.ttml`, `${lang}.srv3`];
|
|
100
102
|
let foundFile;
|
|
101
103
|
for (const ext of possibleExts) {
|
|
102
104
|
const candidate = `${outTemplate}.${ext}`;
|