clipai-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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # ClipAI MCP server
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that exposes the ClipAI clipping
4
+ pipeline as agent-callable tools, so Claude (or any MCP client) can run the whole
5
+ loop from a message: **import a YouTube video, list the AI-detected clips, render
6
+ one, and schedule it to a connected account.**
7
+
8
+ This is ClipAI's answer to agent-triggered clipping: the same generate -> distribute
9
+ loop the app does, now drivable by an agent.
10
+
11
+ ## Tools
12
+
13
+ | Tool | What it does |
14
+ |------|--------------|
15
+ | `import_youtube` | Import a YouTube URL and start AI clip detection (returns video_id, job_id). |
16
+ | `list_videos` | List the org's videos with status and clip counts. |
17
+ | `list_clips` | List clips (optionally for one video): id, title, status, score, start/end. |
18
+ | `get_clip` | Fetch one clip (poll its status after rendering). |
19
+ | `render_clip` | Render a clip to a 9:16 short with burned captions (returns jobId). |
20
+ | `list_social_accounts` | List connected social accounts to schedule to. |
21
+ | `schedule_clip` | Schedule a rendered clip (or `publish_now`) to an account. |
22
+
23
+ A typical agent flow: `import_youtube` -> poll `list_clips(video_id)` until clips
24
+ appear -> `render_clip` -> poll `get_clip` until `status: "READY"` ->
25
+ `list_social_accounts` -> `schedule_clip`.
26
+
27
+ ## Auth
28
+
29
+ The server authenticates with a per-org **API key** (a revocable key you generate in
30
+ ClipAI under **Settings -> API keys**), sent as `Authorization: Bearer <key>`. Set:
31
+
32
+ - `CLIPAI_API_KEY` - the key (starts with `clip_`)
33
+ - `CLIPAI_BASE_URL` - optional, defaults to `https://clipai.online`
34
+
35
+ The key carries the org owner's authority and can be revoked at any time from
36
+ Settings. The same key works for any programmatic API call, not just this server.
37
+
38
+ ## Build
39
+
40
+ ```bash
41
+ cd apps/mcp
42
+ pnpm install # from repo root (workspace) or here
43
+ pnpm build # tsc -> dist/
44
+ ```
45
+
46
+ ## Configure in Claude Desktop / Claude Code
47
+
48
+ Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "clipai": {
54
+ "command": "node",
55
+ "args": ["/absolute/path/to/clipping-app/apps/mcp/dist/index.js"],
56
+ "env": {
57
+ "CLIPAI_API_KEY": "clip_xxxxxxxxxxxxxxxxxxxx",
58
+ "CLIPAI_BASE_URL": "https://clipai.online"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Then ask: *"Clip this YouTube video to 9:16 with a hook, then schedule the best one
66
+ to my TikTok."* The agent will call the tools above in sequence.
package/dist/client.js ADDED
@@ -0,0 +1,55 @@
1
+ export class ClipAIClient {
2
+ cfg;
3
+ constructor(cfg) {
4
+ this.cfg = cfg;
5
+ }
6
+ url(path) {
7
+ return new URL(path, this.cfg.baseUrl).toString();
8
+ }
9
+ async request(method, path, jsonBody) {
10
+ const res = await fetch(this.url(path), {
11
+ method,
12
+ headers: {
13
+ authorization: `Bearer ${this.cfg.apiKey}`,
14
+ ...(jsonBody !== undefined ? { "content-type": "application/json" } : {}),
15
+ },
16
+ body: jsonBody !== undefined ? JSON.stringify(jsonBody) : undefined,
17
+ });
18
+ const text = await res.text();
19
+ let data;
20
+ try {
21
+ data = text ? JSON.parse(text) : null;
22
+ }
23
+ catch {
24
+ data = text;
25
+ }
26
+ if (!res.ok) {
27
+ const msg = data && typeof data === "object" && "error" in data
28
+ ? String(data.error)
29
+ : `HTTP ${res.status}`;
30
+ throw new Error(`${method} ${path} failed: ${msg}`);
31
+ }
32
+ return data;
33
+ }
34
+ importYouTube(url, clipOptions) {
35
+ return this.request("POST", "/api/import", { url, ...(clipOptions ? { clip_options: clipOptions } : {}) });
36
+ }
37
+ listVideos() {
38
+ return this.request("GET", "/api/videos");
39
+ }
40
+ listClips(videoId) {
41
+ return this.request("GET", `/api/clips${videoId ? `?videoId=${encodeURIComponent(videoId)}` : ""}`);
42
+ }
43
+ getClip(clipId) {
44
+ return this.request("GET", `/api/clips/${encodeURIComponent(clipId)}`);
45
+ }
46
+ renderClip(clipId, opts) {
47
+ return this.request("POST", `/api/clips/${encodeURIComponent(clipId)}/render`, opts);
48
+ }
49
+ listSocialAccounts() {
50
+ return this.request("GET", "/api/social/accounts");
51
+ }
52
+ scheduleClip(payload) {
53
+ return this.request("POST", "/api/schedules", payload);
54
+ }
55
+ }
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClipAI MCP server. Exposes the ClipAI clipping pipeline as agent-callable tools
4
+ * so Claude (or any MCP client) can run the whole loop from a message:
5
+ * import a YouTube video -> list the AI-detected clips -> render one -> schedule it.
6
+ *
7
+ * Auth is the org owner's email/password (env), used via the existing session API.
8
+ * Configure in an MCP client (e.g. Claude Desktop) - see README.md.
9
+ */
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
+ import { ClipAIClient } from "./client.js";
14
+ const baseUrl = process.env.CLIPAI_BASE_URL ?? "https://clipai.online";
15
+ const apiKey = process.env.CLIPAI_API_KEY ?? "";
16
+ const client = new ClipAIClient({ baseUrl, apiKey });
17
+ const tools = [
18
+ {
19
+ name: "import_youtube",
20
+ description: "Import a YouTube video into ClipAI and start AI clip detection. Returns a video_id and job_id. " +
21
+ "Detection runs asynchronously; poll list_clips(video_id) until clips appear (usually under a few minutes).",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ url: { type: "string", description: "A YouTube URL (youtube.com/watch, youtu.be, or /shorts/)." },
26
+ goal: {
27
+ type: "string",
28
+ enum: ["viral", "highlights", "insights"],
29
+ description: "Optional steering for what kind of clips to favor.",
30
+ },
31
+ max_clips: { type: "number", description: "Optional cap on how many clips to detect." },
32
+ },
33
+ required: ["url"],
34
+ },
35
+ },
36
+ {
37
+ name: "list_videos",
38
+ description: "List the org's videos (most recent first) with status and clip counts.",
39
+ inputSchema: { type: "object", properties: {} },
40
+ },
41
+ {
42
+ name: "list_clips",
43
+ description: "List clips, optionally filtered to one video_id. Each clip has id, title, status, score (0-1 virality), " +
44
+ "startTime, endTime. A clip must reach status READY (via render_clip) before it can be scheduled.",
45
+ inputSchema: { type: "object", properties: { video_id: { type: "string" } } },
46
+ },
47
+ {
48
+ name: "get_clip",
49
+ description: "Get one clip by id (use to poll status after render_clip).",
50
+ inputSchema: { type: "object", properties: { clip_id: { type: "string" } }, required: ["clip_id"] },
51
+ },
52
+ {
53
+ name: "render_clip",
54
+ description: "Render a clip into a vertical short: captions burned in, auto-reframed to 9:16. Returns a jobId; " +
55
+ "poll get_clip until status is READY. Rendering is required before scheduling.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ clip_id: { type: "string" },
60
+ platform: {
61
+ type: "string",
62
+ enum: ["tiktok", "instagram", "youtube", "linkedin", "x", "threads", "bluesky"],
63
+ description: "Target platform (defaults to tiktok).",
64
+ },
65
+ caption_language: { type: "string", description: "Optional: translate captions to this language." },
66
+ dub_language: { type: "string", description: "Optional: dub the audio into this language (ElevenLabs)." },
67
+ hook_text: { type: "string", description: "Optional headline burned over the first few seconds." },
68
+ reframe_focus: { type: "string", enum: ["center", "left", "right"] },
69
+ remove_silence: { type: "boolean" },
70
+ layout: { type: "string", enum: ["fill", "split"] },
71
+ },
72
+ required: ["clip_id"],
73
+ },
74
+ },
75
+ {
76
+ name: "list_social_accounts",
77
+ description: "List connected social accounts (id, platform, accountName) available to schedule to.",
78
+ inputSchema: { type: "object", properties: {} },
79
+ },
80
+ {
81
+ name: "schedule_clip",
82
+ description: "Schedule a rendered (READY) clip to a connected social account. Pass publish_now=true to post immediately, " +
83
+ "or scheduled_at (ISO 8601) for a future time.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ clip_id: { type: "string" },
88
+ social_account_id: { type: "string" },
89
+ caption: { type: "string" },
90
+ hashtags: { type: "array", items: { type: "string" } },
91
+ scheduled_at: { type: "string", description: "ISO 8601 datetime; ignored when publish_now is true." },
92
+ publish_now: { type: "boolean" },
93
+ },
94
+ required: ["clip_id", "social_account_id", "caption"],
95
+ },
96
+ },
97
+ ];
98
+ const server = new Server({ name: "clipai", version: "0.1.0" }, { capabilities: { tools: {} } });
99
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
100
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
101
+ const name = req.params.name;
102
+ const args = (req.params.arguments ?? {});
103
+ try {
104
+ let result;
105
+ switch (name) {
106
+ case "import_youtube": {
107
+ const clipOptions = {};
108
+ if (args.goal)
109
+ clipOptions.goal = args.goal;
110
+ if (typeof args.max_clips === "number")
111
+ clipOptions.max_clips = args.max_clips;
112
+ result = await client.importYouTube(String(args.url), Object.keys(clipOptions).length ? clipOptions : undefined);
113
+ break;
114
+ }
115
+ case "list_videos":
116
+ result = await client.listVideos();
117
+ break;
118
+ case "list_clips":
119
+ result = await client.listClips(args.video_id ? String(args.video_id) : undefined);
120
+ break;
121
+ case "get_clip":
122
+ result = await client.getClip(String(args.clip_id));
123
+ break;
124
+ case "render_clip": {
125
+ const opts = {};
126
+ if (args.platform)
127
+ opts.platform = args.platform;
128
+ if (args.caption_language)
129
+ opts.captionLanguage = args.caption_language;
130
+ if (args.dub_language)
131
+ opts.dubLanguage = args.dub_language;
132
+ if (args.hook_text)
133
+ opts.hookText = args.hook_text;
134
+ if (args.reframe_focus)
135
+ opts.reframeFocus = args.reframe_focus;
136
+ if (typeof args.remove_silence === "boolean")
137
+ opts.removeSilence = args.remove_silence;
138
+ if (args.layout)
139
+ opts.layout = args.layout;
140
+ result = await client.renderClip(String(args.clip_id), opts);
141
+ break;
142
+ }
143
+ case "list_social_accounts":
144
+ result = await client.listSocialAccounts();
145
+ break;
146
+ case "schedule_clip": {
147
+ const payload = {
148
+ clipId: String(args.clip_id),
149
+ socialAccountId: String(args.social_account_id),
150
+ caption: String(args.caption),
151
+ hashtags: Array.isArray(args.hashtags) ? args.hashtags : [],
152
+ // The schedule API requires a datetime even for publish_now (it overrides
153
+ // it to "now"), so always send a valid ISO string.
154
+ scheduledAt: args.scheduled_at ? String(args.scheduled_at) : new Date().toISOString(),
155
+ ...(args.publish_now ? { publishNow: true } : {}),
156
+ };
157
+ result = await client.scheduleClip(payload);
158
+ break;
159
+ }
160
+ default:
161
+ throw new Error(`Unknown tool: ${name}`);
162
+ }
163
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
164
+ }
165
+ catch (err) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
168
+ }
169
+ });
170
+ async function main() {
171
+ if (!apiKey) {
172
+ console.error("ClipAI MCP: set CLIPAI_API_KEY (generate one in ClipAI Settings -> API keys).");
173
+ process.exit(1);
174
+ }
175
+ const transport = new StdioServerTransport();
176
+ await server.connect(transport);
177
+ console.error(`ClipAI MCP server running against ${baseUrl}`);
178
+ }
179
+ main().catch((err) => {
180
+ console.error(err);
181
+ process.exit(1);
182
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "clipai-mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "MCP server exposing the ClipAI clipping pipeline as agent-callable tools (import, clip, render, schedule).",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "clipai",
11
+ "video",
12
+ "clips",
13
+ "ai",
14
+ "claude"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/isi1314/clipping-app.git",
19
+ "directory": "apps/mcp"
20
+ },
21
+ "bin": {
22
+ "clipai-mcp": "./dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "start": "node dist/index.js",
34
+ "type-check": "tsc --noEmit",
35
+ "prepublishOnly": "tsc"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.12.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.10.0",
42
+ "typescript": "^5.7.0"
43
+ }
44
+ }