@mkterswingman/5mghost-yonder 0.0.1
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/README.md +19 -0
- package/dist/auth/oauthFlow.d.ts +6 -0
- package/dist/auth/oauthFlow.js +151 -0
- package/dist/auth/tokenManager.d.ts +9 -0
- package/dist/auth/tokenManager.js +100 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +92 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +24 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +318 -0
- package/dist/cli/setupCookies.d.ts +34 -0
- package/dist/cli/setupCookies.js +196 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.js +79 -0
- package/dist/tools/remote.d.ts +4 -0
- package/dist/tools/remote.js +230 -0
- package/dist/tools/subtitles.d.ts +4 -0
- package/dist/tools/subtitles.js +579 -0
- package/dist/utils/config.d.ts +23 -0
- package/dist/utils/config.js +47 -0
- package/dist/utils/cookieRefresh.d.ts +18 -0
- package/dist/utils/cookieRefresh.js +70 -0
- package/dist/utils/cookies.d.ts +18 -0
- package/dist/utils/cookies.js +78 -0
- package/dist/utils/launcher.d.ts +12 -0
- package/dist/utils/launcher.js +82 -0
- package/dist/utils/mcpRegistration.d.ts +7 -0
- package/dist/utils/mcpRegistration.js +23 -0
- package/dist/utils/videoInput.d.ts +5 -0
- package/dist/utils/videoInput.js +55 -0
- package/dist/utils/ytdlp.d.ts +7 -0
- package/dist/utils/ytdlp.js +52 -0
- package/dist/utils/ytdlpPath.d.ts +26 -0
- package/dist/utils/ytdlpPath.js +78 -0
- package/package.json +43 -0
- package/scripts/download-ytdlp.mjs +138 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 5mghost-yonder
|
|
2
|
+
|
|
3
|
+
Internal MCP server package for 5mghost workflows.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @mkterswingman/5mghost-yonder setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The setup command configures auth, optional YouTube cookies, and prints MCP config for supported AI clients.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
- `setup` — first-time setup
|
|
16
|
+
- `serve` — start the stdio MCP server
|
|
17
|
+
- `setup-cookies` — refresh YouTube cookies
|
|
18
|
+
- `update` — update to the latest npm version
|
|
19
|
+
- `version` — print the installed version
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
function base64url(buf) {
|
|
5
|
+
return buf
|
|
6
|
+
.toString("base64")
|
|
7
|
+
.replace(/\+/g, "-")
|
|
8
|
+
.replace(/\//g, "_")
|
|
9
|
+
.replace(/=+$/, "");
|
|
10
|
+
}
|
|
11
|
+
export async function runOAuthFlow(authUrl) {
|
|
12
|
+
// 1. Generate PKCE + state
|
|
13
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
14
|
+
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
15
|
+
const state = base64url(randomBytes(32));
|
|
16
|
+
// 2. Start temp HTTP server to get the actual port for redirect_uri
|
|
17
|
+
const { server: httpServer, port } = await startCallbackServer();
|
|
18
|
+
const redirectUri = `http://127.0.0.1:${port}`;
|
|
19
|
+
// 3. DCR register client with actual redirect_uri (including port)
|
|
20
|
+
let clientId;
|
|
21
|
+
let clientSecret;
|
|
22
|
+
try {
|
|
23
|
+
const dcrRes = await fetch(`${authUrl}/oauth/register`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
client_name: "yt-mcp-cli",
|
|
28
|
+
redirect_uris: [redirectUri],
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
if (!dcrRes.ok) {
|
|
32
|
+
const text = await dcrRes.text().catch(() => "");
|
|
33
|
+
httpServer.close();
|
|
34
|
+
throw new Error(`DCR registration failed: ${dcrRes.status} ${text}`);
|
|
35
|
+
}
|
|
36
|
+
const dcrBody = (await dcrRes.json());
|
|
37
|
+
clientId = dcrBody.client_id;
|
|
38
|
+
clientSecret = dcrBody.client_secret;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
httpServer.close();
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
// 4. Wait for OAuth callback
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
httpServer.close();
|
|
48
|
+
reject(new Error("OAuth flow timed out after 5 minutes"));
|
|
49
|
+
}, 5 * 60 * 1000);
|
|
50
|
+
function cleanup() {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
httpServer.close();
|
|
53
|
+
}
|
|
54
|
+
httpServer.on("request", async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
57
|
+
const code = url.searchParams.get("code");
|
|
58
|
+
const error = url.searchParams.get("error");
|
|
59
|
+
const returnedState = url.searchParams.get("state");
|
|
60
|
+
if (error) {
|
|
61
|
+
const safeError = error.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
62
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
63
|
+
res.end(`<h1>Authorization failed</h1><p>${safeError}</p>`);
|
|
64
|
+
cleanup();
|
|
65
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!code) {
|
|
69
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
70
|
+
res.end("<h1>Waiting for authorization...</h1>");
|
|
71
|
+
return;
|
|
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
|
+
}
|
|
81
|
+
// Exchange code for tokens
|
|
82
|
+
const tokenBody = {
|
|
83
|
+
grant_type: "authorization_code",
|
|
84
|
+
code,
|
|
85
|
+
redirect_uri: redirectUri,
|
|
86
|
+
client_id: clientId,
|
|
87
|
+
code_verifier: codeVerifier,
|
|
88
|
+
};
|
|
89
|
+
if (clientSecret) {
|
|
90
|
+
tokenBody.client_secret = clientSecret;
|
|
91
|
+
}
|
|
92
|
+
const tokenRes = await fetch(`${authUrl}/oauth/token`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify(tokenBody),
|
|
96
|
+
});
|
|
97
|
+
if (!tokenRes.ok) {
|
|
98
|
+
const text = await tokenRes.text().catch(() => "");
|
|
99
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
100
|
+
res.end(`<h1>Token exchange failed</h1><p>${text}</p>`);
|
|
101
|
+
cleanup();
|
|
102
|
+
reject(new Error(`Token exchange failed: ${tokenRes.status} ${text}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const tokens = (await tokenRes.json());
|
|
106
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
107
|
+
res.end("<h1>Authorization successful!</h1><p>You can close this window.</p>");
|
|
108
|
+
cleanup();
|
|
109
|
+
resolve({
|
|
110
|
+
accessToken: tokens.access_token,
|
|
111
|
+
refreshToken: tokens.refresh_token,
|
|
112
|
+
expiresIn: tokens.expires_in,
|
|
113
|
+
clientId,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
res.writeHead(500);
|
|
118
|
+
res.end("Internal error");
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(err);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// Open browser
|
|
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)}`;
|
|
125
|
+
console.log("\n\x1b[1mOpen this URL in your browser to authorize:\x1b[0m");
|
|
126
|
+
console.log(`\n ${authorizeUrl}\n`);
|
|
127
|
+
import("node:child_process").then(({ exec }) => {
|
|
128
|
+
const cmd = process.platform === "darwin" ? "open" :
|
|
129
|
+
process.platform === "win32" ? "start" :
|
|
130
|
+
"xdg-open";
|
|
131
|
+
exec(`${cmd} "${authorizeUrl}"`);
|
|
132
|
+
}).catch(() => {
|
|
133
|
+
// ignore — user can open manually
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Start an HTTP server on a random port and return the server + port. */
|
|
138
|
+
async function startCallbackServer() {
|
|
139
|
+
const server = createServer();
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
server.listen(0, "127.0.0.1", () => {
|
|
142
|
+
const addr = server.address();
|
|
143
|
+
if (!addr || typeof addr === "string") {
|
|
144
|
+
server.close();
|
|
145
|
+
reject(new Error("Failed to start callback server"));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
resolve({ server, port: addr.port });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class TokenManager {
|
|
2
|
+
private authUrl;
|
|
3
|
+
constructor(authUrl: string);
|
|
4
|
+
getValidToken(): Promise<string | null>;
|
|
5
|
+
saveTokens(accessToken: string, refreshToken: string, expiresIn: number, clientId?: string): Promise<void>;
|
|
6
|
+
savePAT(pat: string): Promise<void>;
|
|
7
|
+
private readAuth;
|
|
8
|
+
private refreshTokens;
|
|
9
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { PATHS, ensureConfigDir } from "../utils/config.js";
|
|
3
|
+
export class TokenManager {
|
|
4
|
+
authUrl;
|
|
5
|
+
constructor(authUrl) {
|
|
6
|
+
this.authUrl = authUrl;
|
|
7
|
+
}
|
|
8
|
+
async getValidToken() {
|
|
9
|
+
// Check env var PAT first
|
|
10
|
+
const envPat = process.env.YT_MCP_TOKEN;
|
|
11
|
+
if (envPat)
|
|
12
|
+
return envPat;
|
|
13
|
+
const auth = this.readAuth();
|
|
14
|
+
if (!auth)
|
|
15
|
+
return null;
|
|
16
|
+
if (auth.type === "pat" && auth.pat) {
|
|
17
|
+
return auth.pat;
|
|
18
|
+
}
|
|
19
|
+
if (auth.type === "jwt") {
|
|
20
|
+
if (auth.access_token && auth.expires_at && Date.now() < auth.expires_at) {
|
|
21
|
+
return auth.access_token;
|
|
22
|
+
}
|
|
23
|
+
// Try refresh
|
|
24
|
+
if (auth.refresh_token) {
|
|
25
|
+
try {
|
|
26
|
+
const refreshed = await this.refreshTokens(auth.refresh_token, auth.client_id);
|
|
27
|
+
if (refreshed) {
|
|
28
|
+
await this.saveTokens(refreshed.access_token, refreshed.refresh_token, refreshed.expires_in, auth.client_id);
|
|
29
|
+
return refreshed.access_token;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// refresh failed → AUTH_EXPIRED
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async saveTokens(accessToken, refreshToken, expiresIn, clientId) {
|
|
41
|
+
ensureConfigDir();
|
|
42
|
+
const data = {
|
|
43
|
+
type: "jwt",
|
|
44
|
+
access_token: accessToken,
|
|
45
|
+
refresh_token: refreshToken,
|
|
46
|
+
expires_at: Date.now() + expiresIn * 1000,
|
|
47
|
+
client_id: clientId,
|
|
48
|
+
};
|
|
49
|
+
writeFileSync(PATHS.authJson, JSON.stringify(data, null, 2), {
|
|
50
|
+
mode: 0o600,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async savePAT(pat) {
|
|
54
|
+
ensureConfigDir();
|
|
55
|
+
const data = {
|
|
56
|
+
type: "pat",
|
|
57
|
+
pat,
|
|
58
|
+
};
|
|
59
|
+
writeFileSync(PATHS.authJson, JSON.stringify(data, null, 2), {
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
readAuth() {
|
|
64
|
+
if (!existsSync(PATHS.authJson))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(PATHS.authJson, "utf8");
|
|
68
|
+
return JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async refreshTokens(refreshToken, clientId) {
|
|
75
|
+
const url = `${this.authUrl}/oauth/token`;
|
|
76
|
+
const body = {
|
|
77
|
+
grant_type: "refresh_token",
|
|
78
|
+
refresh_token: refreshToken,
|
|
79
|
+
};
|
|
80
|
+
if (clientId) {
|
|
81
|
+
body.client_id = clientId;
|
|
82
|
+
}
|
|
83
|
+
const res = await fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const body = (await res.json());
|
|
92
|
+
if (!body.access_token)
|
|
93
|
+
return null;
|
|
94
|
+
return body;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
8
|
+
function getVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
11
|
+
return pkg.version ?? "unknown";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const command = process.argv[2];
|
|
18
|
+
async function main() {
|
|
19
|
+
switch (command) {
|
|
20
|
+
case "setup": {
|
|
21
|
+
const { runSetup } = await import("./setup.js");
|
|
22
|
+
await runSetup();
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "serve": {
|
|
26
|
+
const { runServe } = await import("./serve.js");
|
|
27
|
+
await runServe();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case "setup-cookies": {
|
|
31
|
+
const { runSetupCookies } = await import("./setupCookies.js");
|
|
32
|
+
await runSetupCookies();
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "update": {
|
|
36
|
+
const current = getVersion();
|
|
37
|
+
console.log(`Current version: ${current}`);
|
|
38
|
+
console.log("Checking for updates...\n");
|
|
39
|
+
try {
|
|
40
|
+
const latest = execSync("npm view @mkterswingman/5mghost-yonder version", {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (latest === current) {
|
|
45
|
+
console.log(`✅ Already on the latest version (${current})`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(`New version available: ${latest}`);
|
|
49
|
+
console.log("Updating...\n");
|
|
50
|
+
execSync("npm install -g @mkterswingman/5mghost-yonder@latest", {
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
});
|
|
53
|
+
console.log(`\n✅ Updated to ${latest}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
console.error(`❌ Update failed: ${msg}`);
|
|
59
|
+
console.error("Try manually: npm install -g @mkterswingman/5mghost-yonder@latest");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "version":
|
|
65
|
+
case "--version":
|
|
66
|
+
case "-v": {
|
|
67
|
+
console.log(getVersion());
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
console.log(`
|
|
72
|
+
yt-mcp v${getVersion()}
|
|
73
|
+
|
|
74
|
+
Usage: yt-mcp <command>
|
|
75
|
+
|
|
76
|
+
Commands:
|
|
77
|
+
setup Run first-time setup (OAuth + cookies + MCP registration)
|
|
78
|
+
serve Start the MCP server (stdio transport)
|
|
79
|
+
setup-cookies Refresh YouTube cookies using browser login
|
|
80
|
+
update Update to the latest version
|
|
81
|
+
version Show current version
|
|
82
|
+
|
|
83
|
+
Environment variables:
|
|
84
|
+
YT_MCP_TOKEN Personal Access Token (skips OAuth)
|
|
85
|
+
`);
|
|
86
|
+
process.exit(command ? 1 : 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
main().catch((err) => {
|
|
90
|
+
console.error("Fatal error:", err);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runServe(): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { loadConfig } from "../utils/config.js";
|
|
3
|
+
import { TokenManager } from "../auth/tokenManager.js";
|
|
4
|
+
import { createServer } from "../server.js";
|
|
5
|
+
export async function runServe() {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const tokenManager = new TokenManager(config.auth_url);
|
|
8
|
+
// PAT mode via env var (don't persist — just keep in memory for this session)
|
|
9
|
+
const pat = process.env.YT_MCP_TOKEN;
|
|
10
|
+
if (pat) {
|
|
11
|
+
await tokenManager.savePAT(pat);
|
|
12
|
+
}
|
|
13
|
+
const server = await createServer(config, tokenManager);
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
await server.connect(transport);
|
|
16
|
+
process.on("SIGINT", async () => {
|
|
17
|
+
await server.close();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
|
20
|
+
process.on("SIGTERM", async () => {
|
|
21
|
+
await server.close();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSetup(): Promise<void>;
|