opencaptions 0.0.0-stage → 1.2.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/dist/mcp.js ADDED
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The MCP server: the same actions as the command line, as tools an agent calls over stdio. A tool
3
+ * that waits for the server reports progress, and stops waiting before clients give up on a call:
4
+ * it hands back a ticket instead, and `check_progress` carries on from it.
5
+ */
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
+ import { captionVideo, defaultOutput, exportSubtitles, fixWords, getProject, readTranscript, resume, saveVideo, setStyle, } from "./captions.js";
10
+ import { api, connectionFromEnv, OpenCaptionsError } from "./client.js";
11
+ import { describeLook, describeStyle, LOOKS, SAVE_SCHEMA, STYLE_SCHEMA } from "./looks.js";
12
+ import { VERSION } from "./version.js";
13
+ /** How long a tool waits for the server before handing back a ticket, in seconds. */
14
+ const WAIT_S = Number(process.env.OPENCAPTIONS_WAIT_SECONDS) || 45;
15
+ const save = SAVE_SCHEMA.properties;
16
+ const saveProperties = {
17
+ format: { type: "string", description: "mp4 (the default), mp4-hevc, webm or mov (ProRes)" },
18
+ resolution: { ...save.resolution, description: "The short side in pixels, or the source's" },
19
+ frame_rate: { ...save.frame_rate, description: "Frames per second, or the source's" },
20
+ captions_only: {
21
+ type: "boolean",
22
+ description: "The captions alone over solid green, with no sound, to key out in Remotion or a video editor",
23
+ },
24
+ output: {
25
+ type: "string",
26
+ description: "Where to write the file. Default: next to the source, named <name>.captioned",
27
+ },
28
+ };
29
+ const project = { type: "string", description: "The project id a caption_video call returned" };
30
+ const TOOLS = [
31
+ {
32
+ name: "caption_video",
33
+ description: "Adds animated word-by-word captions to a video: transcribes the speech, styles it with a " +
34
+ "look, and saves the captioned video next to the source. Returns its path and a project id " +
35
+ "for follow-ups (get_transcript, fix_words, set_style, save_video). If it takes longer than " +
36
+ "the call allows, it returns a ticket: pass it to check_progress.",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ source: {
41
+ type: "string",
42
+ description: "A video file path, or a direct http(s) link to one",
43
+ },
44
+ look: {
45
+ type: "string",
46
+ description: `A built-in look (list_looks): ${LOOKS.map((l) => l.name).join(", ")}`,
47
+ },
48
+ style: {
49
+ ...STYLE_SCHEMA,
50
+ description: "Style settings over the look (describe_style lists every one)",
51
+ },
52
+ language: { type: "string", description: "ISO 639-1 code, or auto (the default)" },
53
+ model: {
54
+ type: "string",
55
+ description: "A speech model id the server offers; its default if left out",
56
+ },
57
+ ...saveProperties,
58
+ },
59
+ required: ["source"],
60
+ },
61
+ },
62
+ {
63
+ name: "check_progress",
64
+ description: "Carries on with a caption_video or save_video that returned a ticket.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: { ticket: { type: "object", description: "The ticket, as it was returned" } },
68
+ required: ["ticket"],
69
+ },
70
+ },
71
+ {
72
+ name: "list_looks",
73
+ description: "The built-in caption looks, each with its animation, font and colours.",
74
+ inputSchema: { type: "object", properties: {} },
75
+ },
76
+ {
77
+ name: "describe_style",
78
+ description: "Every style setting with its type, range, default and meaning, for caption_video's and " +
79
+ "set_style's `style`.",
80
+ inputSchema: { type: "object", properties: {} },
81
+ },
82
+ {
83
+ name: "get_transcript",
84
+ description: "A project's captions as they show on the video, and every word with its index, to check " +
85
+ "names and spelling before fix_words.",
86
+ inputSchema: { type: "object", properties: { project_id: project }, required: ["project_id"] },
87
+ },
88
+ {
89
+ name: "fix_words",
90
+ description: "Sets words by the index get_transcript gave them, keeping their timing; empty text removes " +
91
+ "a word. Then save_video writes the video again.",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ project_id: project,
96
+ fixes: {
97
+ type: "array",
98
+ items: {
99
+ type: "object",
100
+ properties: { index: { type: "integer", minimum: 0 }, text: { type: "string" } },
101
+ required: ["index", "text"],
102
+ },
103
+ },
104
+ },
105
+ required: ["project_id", "fixes"],
106
+ },
107
+ },
108
+ {
109
+ name: "set_style",
110
+ description: "Changes a project's style: a look, settings over it, or settings alone over the style it " +
111
+ "has. Then save_video writes the video again.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ project_id: project,
116
+ look: { type: "string", description: "A built-in look (list_looks)" },
117
+ style: { ...STYLE_SCHEMA, description: "Style settings (describe_style)" },
118
+ },
119
+ required: ["project_id"],
120
+ },
121
+ },
122
+ {
123
+ name: "save_video",
124
+ description: "Saves a project's captioned video again, after fix_words or set_style, or in another " +
125
+ "format, size or as captions only.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: { project_id: project, ...saveProperties },
129
+ required: ["project_id"],
130
+ },
131
+ },
132
+ {
133
+ name: "export_subtitles",
134
+ description: "Writes a project's captions as SRT or VTT subtitles, or its transcript as JSON.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ project_id: project,
139
+ format: { type: "string", enum: ["srt", "vtt", "json"] },
140
+ output: { type: "string", description: "Where to write it; next to the video by default" },
141
+ },
142
+ required: ["project_id", "format"],
143
+ },
144
+ },
145
+ ];
146
+ /** What a tool says back: the outcome in words, with the details as JSON for the next call. */
147
+ function said(text, details) {
148
+ const body = details === undefined ? text : `${text}\n${JSON.stringify(details, null, 2)}`;
149
+ return { content: [{ type: "text", text: body }] };
150
+ }
151
+ function outcome(result) {
152
+ return result.done
153
+ ? said(`Saved ${result.output}`, { project_id: result.project_id, output: result.output })
154
+ : said(`${result.message} Call check_progress with this ticket.`, { ticket: result.ticket });
155
+ }
156
+ function saveArgs(args) {
157
+ return {
158
+ format: args.format,
159
+ resolution: args.resolution,
160
+ frame_rate: args.frame_rate,
161
+ captions_only: args.captions_only,
162
+ output: args.output,
163
+ };
164
+ }
165
+ async function call(client, name, args, progress) {
166
+ const deadline = Date.now() + WAIT_S * 1000;
167
+ const id = args.project_id;
168
+ switch (name) {
169
+ case "caption_video":
170
+ return outcome(await captionVideo(client, { ...args, ...saveArgs(args) }, progress, deadline));
171
+ case "check_progress":
172
+ return outcome(await resume(client, args.ticket, progress, deadline));
173
+ case "list_looks":
174
+ return said(LOOKS.map(describeLook).join("\n"));
175
+ case "describe_style":
176
+ return said(describeStyle());
177
+ case "get_transcript":
178
+ return said("Captions as shown, then each word with its index:", await readTranscript(client, id));
179
+ case "fix_words":
180
+ return said(`Fixed ${await fixWords(client, id, args.fixes)} words. Call save_video to write the video again.`);
181
+ case "set_style": {
182
+ const style = await setStyle(client, id, args.look, args.style);
183
+ return said("The style is set. Call save_video to write the video again.", style);
184
+ }
185
+ case "save_video": {
186
+ const options = saveArgs(args);
187
+ const stem = defaultOutput((await getProject(client, id)).title, options.captions_only);
188
+ return outcome(await saveVideo(client, id, { ...options, stem }, progress, deadline));
189
+ }
190
+ case "export_subtitles": {
191
+ const kind = args.format;
192
+ const output = args.output ?? defaultOutput((await getProject(client, id)).title);
193
+ return said(`Wrote ${await exportSubtitles(client, id, kind, output)}`);
194
+ }
195
+ default:
196
+ throw new OpenCaptionsError(`There is no tool called ${name}.`);
197
+ }
198
+ }
199
+ export function createServer(connect = () => api(connectionFromEnv())) {
200
+ const server = new Server({ name: "opencaptions", version: VERSION }, {
201
+ capabilities: { tools: {} },
202
+ instructions: "OpenCaptions adds animated word-by-word captions to videos, on the user's own OpenCaptions " +
203
+ "server. caption_video does it in one call; the other tools adjust the result.",
204
+ });
205
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
206
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
207
+ const token = request.params._meta?.progressToken;
208
+ const progress = (fraction, message) => {
209
+ if (token === undefined)
210
+ return;
211
+ void extra.sendNotification({
212
+ method: "notifications/progress",
213
+ params: { progressToken: token, progress: fraction, total: 1, message },
214
+ });
215
+ };
216
+ try {
217
+ return await call(connect(), request.params.name, request.params.arguments ?? {}, progress);
218
+ }
219
+ catch (error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ return { ...said(message), isError: true };
222
+ }
223
+ });
224
+ return server;
225
+ }
226
+ export async function serve() {
227
+ await createServer().connect(new StdioServerTransport());
228
+ }
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from "node:fs";
2
+ /** The package's version, from its manifest: one place to change it, which the release bumps. */
3
+ export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
Binary file
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "opencaptions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "description": "Animated word-by-word captions for your videos, from Claude, Cursor or any MCP client, or the command line. Runs on your own OpenCaptions server.",
5
+ "mcpName": "io.github.leogaudin/opencaptions",
6
+ "license": "AGPL-3.0-only",
7
+ "type": "module",
8
+ "homepage": "https://docs.opencaptions.app/guides/agents/",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/leogaudin/opencaptions.git",
12
+ "directory": "apps/agent"
13
+ },
14
+ "keywords": [
15
+ "captions",
16
+ "subtitles",
17
+ "mcp",
18
+ "video",
19
+ "transcription",
20
+ "self-hosted"
21
+ ],
22
+ "bin": {
23
+ "opencaptions": "dist/cli.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "engine/opencaptions_engine.wasm",
28
+ "server.json",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20.10"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "lint": "biome check src/ tests/",
37
+ "format": "biome format --write src/ tests/",
38
+ "typecheck": "tsc --pretty false",
39
+ "test": "node --test \"tests/**/*.test.ts\""
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "1.32.2",
43
+ "openapi-fetch": "0.17.0"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "2.5.15",
47
+ "@types/node": "^26.6.2",
48
+ "typescript": "^5.9.3"
49
+ }
50
+ }
package/server.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.leogaudin/opencaptions",
4
+ "title": "OpenCaptions",
5
+ "description": "Animated word-by-word captions for your videos, made on your own OpenCaptions server",
6
+ "websiteUrl": "https://docs.opencaptions.app/guides/agents/",
7
+ "repository": {
8
+ "url": "https://github.com/leogaudin/opencaptions",
9
+ "source": "github",
10
+ "subfolder": "apps/agent"
11
+ },
12
+ "version": "1.2.0",
13
+ "packages": [
14
+ {
15
+ "registryType": "npm",
16
+ "registryBaseUrl": "https://registry.npmjs.org",
17
+ "identifier": "opencaptions",
18
+ "version": "1.2.0",
19
+ "transport": { "type": "stdio" },
20
+ "packageArguments": [{ "type": "positional", "value": "mcp" }],
21
+ "environmentVariables": [
22
+ {
23
+ "name": "OPENCAPTIONS_KEY",
24
+ "description": "An API key from your OpenCaptions server's Account page",
25
+ "isRequired": true,
26
+ "isSecret": true
27
+ },
28
+ {
29
+ "name": "OPENCAPTIONS_URL",
30
+ "description": "Your OpenCaptions server's web address",
31
+ "default": "http://localhost:5173"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }