@unpuzzle/viddy-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muscled
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @unpuzzle/viddy-mcp
2
+
3
+ The Model Context Protocol server for [Viddy](https://viddy.lol).
4
+
5
+ Ask Claude about your screen recordings: pull transcripts, find the moment you
6
+ said something, and read the timestamped notes people left on a share link.
7
+
8
+ > If you're on **claude.ai**, you don't need this package — add
9
+ > `https://viddy.lol/mcp` as a Connector instead.
10
+ > See [viddy.lol/install/mcp](https://viddy.lol/install/mcp).
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install -g @unpuzzle/viddy-mcp
16
+ ```
17
+
18
+ Then register it with your MCP client:
19
+
20
+ ### Claude Code
21
+
22
+ ```sh
23
+ claude mcp add viddy -- viddy-mcp
24
+ ```
25
+
26
+ ### Cursor / Claude Desktop
27
+
28
+ Add this to your MCP config (usually `~/.cursor/mcp.json` or your client's
29
+ settings file):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "viddy": {
35
+ "command": "viddy-mcp"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## First-run sign-in
42
+
43
+ The first time Claude calls a tool that needs your account, the MCP opens your
44
+ browser to the Viddy consent screen. Sign in, click **Allow**, and you're done.
45
+ Tokens are saved locally and refreshed automatically:
46
+
47
+ - macOS / Linux: `~/.config/viddy-mcp/auth.json`
48
+ - Windows: `%APPDATA%\viddy-mcp\auth.json`
49
+
50
+ The file is written `chmod 600` (owner-only). Nothing is stored anywhere else,
51
+ and the package never sees your Google password.
52
+
53
+ To revoke access later, visit
54
+ [Settings → Connected Apps](https://viddy.lol/settings/connected-apps) on
55
+ viddy.lol. To just forget the credentials on this machine, ask Claude to
56
+ disconnect.
57
+
58
+ ## Tools
59
+
60
+ Claude picks the right tool from what you ask — you don't need the names.
61
+
62
+ **Videos** — `list_videos`, `get_video`
63
+
64
+ **Transcripts** — `get_transcript`, `search_transcripts`
65
+
66
+ **Feedback** — `list_notes`
67
+
68
+ **Connection** — `ping`, `connect`, `disconnect`, `whoami`
69
+
70
+ ## What you can ask
71
+
72
+ - "What videos did I record this week?"
73
+ - "Summarize my latest Viddy recording."
74
+ - "When did I talk about pricing in my recordings?"
75
+ - "What feedback did people leave on the onboarding walkthrough?"
76
+ - "Pull the transcript between 10 and 20 minutes of that demo and turn it into release notes."
77
+
78
+ Transcripts come from the Viddy desktop app, which transcribes on-device after
79
+ a recording finishes. Videos uploaded through the web app aren't transcribed,
80
+ so `get_transcript` returns status `none` for those.
81
+
82
+ ## Permissions
83
+
84
+ The MCP requests three read-only scopes and nothing else:
85
+
86
+ | Scope | What it reads |
87
+ | --- | --- |
88
+ | `videos.read` | Your video list and metadata |
89
+ | `transcripts.read` | Transcript text and timestamps |
90
+ | `annotations.read` | Timestamped notes on your videos |
91
+
92
+ There is no write scope. This package cannot record, delete, rename, or
93
+ re-share anything.
94
+
95
+ ## Pointing at a different Viddy
96
+
97
+ Set `VIDDY_BASE_URL` to target a non-production instance. Credentials are
98
+ stored per base URL, so a local dev connection won't disturb your production
99
+ one.
100
+
101
+ ```sh
102
+ VIDDY_BASE_URL=http://localhost:3140 viddy-mcp
103
+ ```
104
+
105
+ ## Troubleshooting
106
+
107
+ **"Not connected to Viddy"** — the grant was revoked or the credentials file
108
+ was removed. Ask Claude to connect again.
109
+
110
+ **A tool says it needs a scope you don't have** — the scope list grew since you
111
+ connected. Revoke the grant under Settings → Connected Apps, then reconnect to
112
+ consent to the new list.
113
+
114
+ **The browser never opens** — the consent URL is also printed to stderr; paste
115
+ it in yourself. Set `VIDDY_BROWSER_CMD` to override the browser command.
116
+
117
+ ## License
118
+
119
+ MIT. See `LICENSE`.
package/dist/api.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Authenticated HTTP helper for the Viddy API, and the ApiClient that
3
+ * @viddy/mcp-core tools run against.
4
+ *
5
+ * apiFetch(path, init?):
6
+ * 1. Load stored tokens; with none, run the OAuth flow (first-run consent).
7
+ * 2. Refresh proactively when the access token is near expiry.
8
+ * 3. Send `Authorization: Bearer <access_token>`.
9
+ * 4. On 401, refresh once and replay the request.
10
+ * 5. On 403 insufficient_scope, explain how to re-consent.
11
+ * 6. Return parsed JSON, or throw with a message worth showing the user.
12
+ */
13
+ import type { ApiClient } from "@viddy/mcp-core";
14
+ export interface ApiOptions extends Omit<RequestInit, "headers"> {
15
+ /** Surface "not connected" instead of opening a browser. */
16
+ noAutoAuth?: boolean;
17
+ headers?: Record<string, string>;
18
+ }
19
+ export declare class ApiError extends Error {
20
+ status: number;
21
+ body: unknown;
22
+ constructor(message: string, status: number, body: unknown);
23
+ }
24
+ export declare class NotConnectedError extends Error {
25
+ constructor();
26
+ }
27
+ /**
28
+ * Make an authenticated request. `path` is rooted at the Viddy origin
29
+ * (e.g. "/api/v1/videos?limit=5"). Returns the parsed JSON body.
30
+ */
31
+ export declare function apiFetch<T = unknown>(path: string, init?: ApiOptions): Promise<T>;
32
+ /** The ApiClient handed to @viddy/mcp-core tool handlers. */
33
+ export declare const apiClient: ApiClient;
34
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAWH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC;IAC9D,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,qBAAa,QAAS,SAAQ,KAAK;IAGxB,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,OAAO;gBAFpB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO;CAKvB;AAED,qBAAa,iBAAkB,SAAQ,KAAK;;CAO3C;AA0BD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAgE3F;AAED,6DAA6D;AAC7D,eAAO,MAAM,SAAS,EAAE,SAGvB,CAAC"}
package/dist/api.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Authenticated HTTP helper for the Viddy API, and the ApiClient that
3
+ * @viddy/mcp-core tools run against.
4
+ *
5
+ * apiFetch(path, init?):
6
+ * 1. Load stored tokens; with none, run the OAuth flow (first-run consent).
7
+ * 2. Refresh proactively when the access token is near expiry.
8
+ * 3. Send `Authorization: Bearer <access_token>`.
9
+ * 4. On 401, refresh once and replay the request.
10
+ * 5. On 403 insufficient_scope, explain how to re-consent.
11
+ * 6. Return parsed JSON, or throw with a message worth showing the user.
12
+ */
13
+ import { getBaseUrl } from "./config.js";
14
+ import { accessTokenIsFresh, clearTokens, loadTokens, } from "./auth.js";
15
+ import { refreshTokens, runAuthorizationFlow } from "./oauth.js";
16
+ import { log } from "./log.js";
17
+ export class ApiError extends Error {
18
+ status;
19
+ body;
20
+ constructor(message, status, body) {
21
+ super(message);
22
+ this.status = status;
23
+ this.body = body;
24
+ this.name = "ApiError";
25
+ }
26
+ }
27
+ export class NotConnectedError extends Error {
28
+ constructor() {
29
+ super("Not connected to Viddy. Run the `connect` tool to sign in — or just ask for your videos again and consent will open in your browser.");
30
+ this.name = "NotConnectedError";
31
+ }
32
+ }
33
+ async function ensureTokens(opts) {
34
+ let tokens = await loadTokens();
35
+ if (!tokens) {
36
+ if (opts.noAutoAuth)
37
+ throw new NotConnectedError();
38
+ log("info", "no tokens found; starting OAuth flow");
39
+ tokens = await runAuthorizationFlow();
40
+ }
41
+ if (!accessTokenIsFresh(tokens)) {
42
+ log("info", "access token near expiry; refreshing");
43
+ try {
44
+ tokens = await refreshTokens(tokens.refreshToken);
45
+ }
46
+ catch (e) {
47
+ // Usually means the grant was revoked. Drop the dead tokens, re-consent.
48
+ log("warn", "refresh failed; clearing tokens", {
49
+ error: e instanceof Error ? e.message : String(e),
50
+ });
51
+ await clearTokens();
52
+ if (opts.noAutoAuth)
53
+ throw new NotConnectedError();
54
+ tokens = await runAuthorizationFlow();
55
+ }
56
+ }
57
+ return tokens;
58
+ }
59
+ /**
60
+ * Make an authenticated request. `path` is rooted at the Viddy origin
61
+ * (e.g. "/api/v1/videos?limit=5"). Returns the parsed JSON body.
62
+ */
63
+ export async function apiFetch(path, init = {}) {
64
+ const { noAutoAuth, headers: extraHeaders, ...rest } = init;
65
+ const tokens = await ensureTokens({ noAutoAuth: noAutoAuth ?? false });
66
+ const doRequest = (token) => fetch(`${getBaseUrl()}${path}`, {
67
+ ...rest,
68
+ headers: {
69
+ ...extraHeaders,
70
+ Authorization: `Bearer ${token}`,
71
+ Accept: "application/json",
72
+ },
73
+ });
74
+ let res = await doRequest(tokens.accessToken);
75
+ // 401 → one refresh + replay. A second 401 is a real failure, not a stale token.
76
+ if (res.status === 401) {
77
+ log("info", "got 401; attempting refresh + replay");
78
+ let refreshed;
79
+ try {
80
+ refreshed = await refreshTokens(tokens.refreshToken);
81
+ }
82
+ catch {
83
+ await clearTokens();
84
+ throw new NotConnectedError();
85
+ }
86
+ res = await doRequest(refreshed.accessToken);
87
+ }
88
+ if (!res.ok) {
89
+ let body = null;
90
+ try {
91
+ body = await res.json();
92
+ }
93
+ catch {
94
+ body = await res.text().catch(() => null);
95
+ }
96
+ if (res.status === 403 && typeof body === "object" && body !== null && "reason" in body) {
97
+ const b = body;
98
+ if (b.reason === "insufficient_scope") {
99
+ throw new ApiError(`This tool needs the "${b.required_scope}" scope, which the current connection doesn't have. ` +
100
+ "Revoke Viddy MCP under Settings → Connected Apps on viddy.lol, then run `connect` again to grant it.", 403, body);
101
+ }
102
+ }
103
+ // Lead with the human sentence (error_description) — Claude shows this to
104
+ // the user. The machine code only appears when there is no description.
105
+ const code = typeof body === "object" && body !== null && "error" in body
106
+ ? String(body.error)
107
+ : `HTTP ${res.status}`;
108
+ const description = typeof body === "object" && body !== null && "error_description" in body
109
+ ? String(body.error_description)
110
+ : "";
111
+ throw new ApiError(description || `${code} (${res.status})`, res.status, body);
112
+ }
113
+ if (res.status === 204)
114
+ return undefined;
115
+ return (await res.json());
116
+ }
117
+ /** The ApiClient handed to @viddy/mcp-core tool handlers. */
118
+ export const apiClient = {
119
+ fetch: (path, init) => apiFetch(path, init),
120
+ };
121
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Token storage — load/save the OAuth tokens to disk.
3
+ *
4
+ * Storage path (env-paths, so it's right on every platform):
5
+ * macOS/Linux: ~/.config/viddy-mcp/auth.json
6
+ * Windows: %APPDATA%\viddy-mcp\auth.json
7
+ *
8
+ * The file holds a bearer token for the user's Viddy account, so it is written
9
+ * chmod 600. On Windows, user-profile ACLs already make it owner-only and
10
+ * POSIX modes are meaningless, so chmod failures are ignored.
11
+ *
12
+ * One file holds tokens per base URL, so someone pointing VIDDY_BASE_URL at a
13
+ * local dev server does not clobber their production connection.
14
+ */
15
+ export interface StoredTokens {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ /** Unix ms at which the access token expires. */
19
+ expiresAt: number;
20
+ /** Granted scopes, space-separated, as returned by /api/oauth/token. */
21
+ scope: string;
22
+ /** When the grant was first issued (ms). */
23
+ grantedAt: number;
24
+ }
25
+ /** Tokens for the current base URL, or null if not connected. */
26
+ export declare function loadTokens(): Promise<StoredTokens | null>;
27
+ /** Save tokens for the current base URL, preserving entries for other hosts. */
28
+ export declare function saveTokens(tokens: StoredTokens): Promise<void>;
29
+ /** Forget tokens for the current base URL. Does not revoke server-side. */
30
+ export declare function clearTokens(): Promise<void>;
31
+ /** Where the auth file lives — surfaced in help/debug output. */
32
+ export declare function authFilePath(): string;
33
+ /** Is the access token still usable? 30s safety margin for clock skew + flight time. */
34
+ export declare function accessTokenIsFresh(tokens: StoredTokens): boolean;
35
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB;AA+CD,iEAAiE;AACjE,wBAAsB,UAAU,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAI/D;AAED,gFAAgF;AAChF,wBAAsB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE;AAED,2EAA2E;AAC3E,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjD;AAED,iEAAiE;AACjE,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAEhE"}
package/dist/auth.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Token storage — load/save the OAuth tokens to disk.
3
+ *
4
+ * Storage path (env-paths, so it's right on every platform):
5
+ * macOS/Linux: ~/.config/viddy-mcp/auth.json
6
+ * Windows: %APPDATA%\viddy-mcp\auth.json
7
+ *
8
+ * The file holds a bearer token for the user's Viddy account, so it is written
9
+ * chmod 600. On Windows, user-profile ACLs already make it owner-only and
10
+ * POSIX modes are meaningless, so chmod failures are ignored.
11
+ *
12
+ * One file holds tokens per base URL, so someone pointing VIDDY_BASE_URL at a
13
+ * local dev server does not clobber their production connection.
14
+ */
15
+ import { mkdir, readFile, writeFile, chmod, rename, unlink } from "node:fs/promises";
16
+ import { dirname, join } from "node:path";
17
+ import envPaths from "env-paths";
18
+ import { getBaseUrl } from "./config.js";
19
+ import { log } from "./log.js";
20
+ const PATHS = envPaths("viddy-mcp", { suffix: "" });
21
+ const AUTH_PATH = join(PATHS.config, "auth.json");
22
+ async function readFileOrNull() {
23
+ try {
24
+ const parsed = JSON.parse(await readFile(AUTH_PATH, "utf8"));
25
+ if (parsed?.schemaVersion !== 1) {
26
+ log("warn", "auth.json schemaVersion mismatch; ignoring stored tokens", {
27
+ path: AUTH_PATH,
28
+ });
29
+ return null;
30
+ }
31
+ return parsed;
32
+ }
33
+ catch (e) {
34
+ if (e?.code === "ENOENT")
35
+ return null;
36
+ log("warn", "failed to read auth.json; treating as no stored tokens", {
37
+ path: AUTH_PATH,
38
+ error: e instanceof Error ? e.message : String(e),
39
+ });
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Write via temp file + rename so a crash mid-write can't leave a truncated
45
+ * auth.json. chmod happens on the temp file, before it is visible under the
46
+ * real name, so the token is never briefly world-readable.
47
+ */
48
+ async function writeAtomic(file) {
49
+ await mkdir(dirname(AUTH_PATH), { recursive: true, mode: 0o700 });
50
+ const tmp = `${AUTH_PATH}.${process.pid}.tmp`;
51
+ try {
52
+ await writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", mode: 0o600 });
53
+ try {
54
+ await chmod(tmp, 0o600);
55
+ }
56
+ catch {
57
+ // Best-effort: Windows has no POSIX modes.
58
+ }
59
+ await rename(tmp, AUTH_PATH);
60
+ }
61
+ catch (e) {
62
+ await unlink(tmp).catch(() => { });
63
+ throw e;
64
+ }
65
+ }
66
+ /** Tokens for the current base URL, or null if not connected. */
67
+ export async function loadTokens() {
68
+ const file = await readFileOrNull();
69
+ if (!file)
70
+ return null;
71
+ return file.byBaseUrl[getBaseUrl()] ?? null;
72
+ }
73
+ /** Save tokens for the current base URL, preserving entries for other hosts. */
74
+ export async function saveTokens(tokens) {
75
+ const file = (await readFileOrNull()) ?? { schemaVersion: 1, byBaseUrl: {} };
76
+ file.byBaseUrl[getBaseUrl()] = tokens;
77
+ await writeAtomic(file);
78
+ }
79
+ /** Forget tokens for the current base URL. Does not revoke server-side. */
80
+ export async function clearTokens() {
81
+ const file = await readFileOrNull();
82
+ if (!file)
83
+ return;
84
+ delete file.byBaseUrl[getBaseUrl()];
85
+ await writeAtomic(file);
86
+ }
87
+ /** Where the auth file lives — surfaced in help/debug output. */
88
+ export function authFilePath() {
89
+ return AUTH_PATH;
90
+ }
91
+ /** Is the access token still usable? 30s safety margin for clock skew + flight time. */
92
+ export function accessTokenIsFresh(tokens) {
93
+ return tokens.expiresAt - Date.now() > 30_000;
94
+ }
95
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Compile-time configuration for the Viddy MCP server.
3
+ *
4
+ * CLIENT_ID is a public, hardcoded value that must match the `viddy-mcp` row
5
+ * seeded into the oauth_client table by the Viddy web app's migration. It is
6
+ * not a secret — this is a public PKCE client (RFC 8252), where the proof of
7
+ * possession is the code verifier, not a client secret.
8
+ */
9
+ export declare const CLIENT_ID = "viddy-mcp";
10
+ export declare const DEFAULT_BASE_URL = "https://viddy.lol";
11
+ /** Resolve the Viddy base URL: VIDDY_BASE_URL override → production default. */
12
+ export declare function getBaseUrl(): string;
13
+ /**
14
+ * Scopes requested at install. Must be a subset of the scopes registered on
15
+ * the oauth_client row. Changing this list means existing users have to
16
+ * re-consent on their next connect.
17
+ */
18
+ export declare const REQUESTED_SCOPES: readonly ["videos.read", "transcripts.read", "annotations.read"];
19
+ export declare const PACKAGE_NAME = "@unpuzzle/viddy-mcp";
20
+ /** Sent in the MCP `serverInfo`. Keep in sync with package.json `version`. */
21
+ export declare const PACKAGE_VERSION = "0.1.0";
22
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,kEAInB,CAAC;AAEX,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAElD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,UAAU,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Compile-time configuration for the Viddy MCP server.
3
+ *
4
+ * CLIENT_ID is a public, hardcoded value that must match the `viddy-mcp` row
5
+ * seeded into the oauth_client table by the Viddy web app's migration. It is
6
+ * not a secret — this is a public PKCE client (RFC 8252), where the proof of
7
+ * possession is the code verifier, not a client secret.
8
+ */
9
+ export const CLIENT_ID = "viddy-mcp";
10
+ export const DEFAULT_BASE_URL = "https://viddy.lol";
11
+ /** Resolve the Viddy base URL: VIDDY_BASE_URL override → production default. */
12
+ export function getBaseUrl() {
13
+ const fromEnv = process.env["VIDDY_BASE_URL"];
14
+ if (fromEnv && fromEnv.length > 0)
15
+ return fromEnv.replace(/\/+$/, "");
16
+ return DEFAULT_BASE_URL;
17
+ }
18
+ /**
19
+ * Scopes requested at install. Must be a subset of the scopes registered on
20
+ * the oauth_client row. Changing this list means existing users have to
21
+ * re-consent on their next connect.
22
+ */
23
+ export const REQUESTED_SCOPES = [
24
+ "videos.read",
25
+ "transcripts.read",
26
+ "annotations.read",
27
+ ];
28
+ export const PACKAGE_NAME = "@unpuzzle/viddy-mcp";
29
+ /** Sent in the MCP `serverInfo`. Keep in sync with package.json `version`. */
30
+ export const PACKAGE_VERSION = "0.1.0";
31
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAErC,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEpD,gFAAgF;AAChF,MAAM,UAAU,UAAU;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC9C,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACtE,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,aAAa;IACb,kBAAkB;IAClB,kBAAkB;CACV,CAAC;AAEX,MAAM,CAAC,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAElD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC"}
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @unpuzzle/viddy-mcp — MCP server for Viddy, spoken over stdio.
4
+ *
5
+ * The host (Claude Code, Claude Desktop, Cursor) spawns this process and talks
6
+ * to it on stdin/stdout. Every diagnostic goes to stderr via ./log.js, because
7
+ * anything on stdout corrupts the protocol framing.
8
+ *
9
+ * Tool definitions live in @viddy/mcp-core so the hosted /mcp endpoint on
10
+ * viddy.lol serves exactly the same surface. The three connection tools below
11
+ * (connect/disconnect/whoami) are package-only: they manage tokens on this
12
+ * machine, which a remote Connector has no equivalent of.
13
+ *
14
+ * Usage:
15
+ * npm install -g @unpuzzle/viddy-mcp
16
+ * claude mcp add viddy -- viddy-mcp
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG"}