creavit-studio-mcp 1.0.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.
@@ -0,0 +1,118 @@
1
+ // MCP sunucusu — stdio JSON-RPC taşıma katmanı.
2
+ // MCP stdio taşıması satır-sınırlı JSON kullanır: her mesaj tek satır.
3
+ // stdout SADECE protokol mesajları içindir; log'lar stderr'e gider.
4
+
5
+ import { JSONRPC_ERRORS, MCP_PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION } from "./protocol.mjs";
6
+
7
+ export function log(...args) {
8
+ // stdout protokole ait; teşhis çıktısı stderr'e.
9
+ console.error("[creavit-mcp]", ...args);
10
+ }
11
+
12
+ function write(message) {
13
+ process.stdout.write(`${JSON.stringify(message)}\n`);
14
+ }
15
+
16
+ function respond(id, result) {
17
+ if (id === undefined || id === null) return; // bildirim: cevap yok
18
+ write({ jsonrpc: "2.0", id, result });
19
+ }
20
+
21
+ function respondError(id, code, message, data) {
22
+ if (id === undefined || id === null) return;
23
+ write({ jsonrpc: "2.0", id, error: { code, message, ...(data ? { data } : {}) } });
24
+ }
25
+
26
+ /**
27
+ * @param {object} opts
28
+ * @param {() => Promise<Array>} opts.listTools
29
+ * @param {(name: string, args: object) => Promise<object>} opts.callTool
30
+ */
31
+ export function startRpcServer({ listTools, callTool }) {
32
+ let buffer = "";
33
+ let initialized = false;
34
+
35
+ const handlers = {
36
+ initialize: async (params) => {
37
+ initialized = true;
38
+ log(`initialize (client: ${params?.clientInfo?.name || "unknown"})`);
39
+ return {
40
+ protocolVersion: MCP_PROTOCOL_VERSION,
41
+ capabilities: { tools: { listChanged: false } },
42
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
43
+ };
44
+ },
45
+
46
+ ping: async () => ({}),
47
+
48
+ "tools/list": async () => ({ tools: await listTools() }),
49
+
50
+ "tools/call": async (params) => {
51
+ const name = params?.name;
52
+ if (!name) {
53
+ const error = new Error("`name` is required");
54
+ error.rpcCode = JSONRPC_ERRORS.INVALID_PARAMS;
55
+ throw error;
56
+ }
57
+ return callTool(name, params?.arguments || {});
58
+ },
59
+
60
+ // Bu sunucu kaynak/prompt sunmuyor; istemciler yine de sorabiliyor.
61
+ "resources/list": async () => ({ resources: [] }),
62
+ "prompts/list": async () => ({ prompts: [] }),
63
+ };
64
+
65
+ async function handleMessage(message) {
66
+ const { id, method, params } = message || {};
67
+
68
+ if (!method) return; // cevap mesajı — bu sunucu istemci değil, yok say
69
+
70
+ if (method.startsWith("notifications/")) {
71
+ if (method === "notifications/initialized") log("client ready");
72
+ return;
73
+ }
74
+
75
+ const handler = handlers[method];
76
+ if (!handler) {
77
+ respondError(id, JSONRPC_ERRORS.METHOD_NOT_FOUND, `Unknown method: ${method}`);
78
+ return;
79
+ }
80
+
81
+ if (!initialized && method !== "initialize") {
82
+ respondError(id, JSONRPC_ERRORS.INVALID_REQUEST, "initialize must be called first");
83
+ return;
84
+ }
85
+
86
+ try {
87
+ respond(id, await handler(params));
88
+ } catch (error) {
89
+ respondError(
90
+ id,
91
+ error?.rpcCode || JSONRPC_ERRORS.INTERNAL_ERROR,
92
+ error?.message || String(error),
93
+ );
94
+ }
95
+ }
96
+
97
+ process.stdin.setEncoding("utf8");
98
+ process.stdin.on("data", (chunk) => {
99
+ buffer += chunk;
100
+ let newlineIndex;
101
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
102
+ const line = buffer.slice(0, newlineIndex).trim();
103
+ buffer = buffer.slice(newlineIndex + 1);
104
+ if (!line) continue;
105
+ try {
106
+ handleMessage(JSON.parse(line));
107
+ } catch (_) {
108
+ respondError(null, JSONRPC_ERRORS.PARSE_ERROR, "Invalid JSON");
109
+ }
110
+ }
111
+ });
112
+
113
+ process.stdin.on("end", () => process.exit(0));
114
+ process.on("SIGINT", () => process.exit(0));
115
+ process.on("SIGTERM", () => process.exit(0));
116
+
117
+ log(`ready (MCP ${MCP_PROTOCOL_VERSION})`);
118
+ }
@@ -0,0 +1,59 @@
1
+ // MCP sunucusu — araç tanımı yardımcıları.
2
+ // Her araç bir köprü komutuna karşılık gelir; burada MCP şeması + sonuç
3
+ // biçimlendirmesi standartlaştırılır.
4
+
5
+ import { callCommand } from "../bridgeClient.mjs";
6
+
7
+ export const S = {
8
+ string: (description, extra = {}) => ({ type: "string", description, ...extra }),
9
+ number: (description, extra = {}) => ({ type: "number", description, ...extra }),
10
+ bool: (description) => ({ type: "boolean", description }),
11
+ object: (description, extra = {}) => ({ type: "object", description, ...extra }),
12
+ array: (description, items = {}) => ({ type: "array", description, items }),
13
+ };
14
+
15
+ export function schema(properties = {}, required = []) {
16
+ return {
17
+ type: "object",
18
+ properties,
19
+ ...(required.length ? { required } : {}),
20
+ additionalProperties: false,
21
+ };
22
+ }
23
+
24
+ export function textResult(value) {
25
+ const text =
26
+ typeof value === "string" ? value : JSON.stringify(value ?? null, null, 2);
27
+ return { content: [{ type: "text", text }] };
28
+ }
29
+
30
+ export function errorResult(error) {
31
+ const parts = [error?.message || String(error)];
32
+ if (error?.code) parts.push(`code: ${error.code}`);
33
+ if (error?.details) parts.push(`details: ${JSON.stringify(error.details)}`);
34
+ return { content: [{ type: "text", text: parts.join("\n") }], isError: true };
35
+ }
36
+
37
+ /**
38
+ * Köprü komutunu doğrudan saran araç.
39
+ * @param {object} def
40
+ * @param {string} def.name MCP araç adı
41
+ * @param {string} def.command Köprü komutu
42
+ * @param {string} def.description
43
+ * @param {object} def.inputSchema
44
+ * @param {(args:object)=>object} [def.mapParams] MCP argümanları → komut parametreleri
45
+ * @param {(result:object, args:object)=>object} [def.mapResult]
46
+ * @param {number} [def.timeoutMs]
47
+ */
48
+ export function bridgeTool(def) {
49
+ return {
50
+ name: def.name,
51
+ description: def.description,
52
+ inputSchema: def.inputSchema || schema(),
53
+ run: async (args) => {
54
+ const params = def.mapParams ? def.mapParams(args || {}) : args || {};
55
+ const result = await callCommand(def.command, params, def.timeoutMs);
56
+ return def.mapResult ? def.mapResult(result, args || {}) : textResult(result);
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,192 @@
1
+ // MCP araçları — editör kontrolü.
2
+ // Hepsi editör penceresinin açık olmasını gerektirir (creavit_project_open).
3
+
4
+ import { bridgeTool, schema, S, textResult } from "./defineTool.mjs";
5
+ import { callCommand } from "../bridgeClient.mjs";
6
+
7
+ export function editorTools() {
8
+ return [
9
+ bridgeTool({
10
+ name: "creavit_editor_state",
11
+ description:
12
+ "Live editor state: open project, duration, playhead position, playback state, video resolution, segment count. Call this before editing and wait until duration is greater than 0 — the project is still loading otherwise, and edits made too early get overwritten.",
13
+ command: "editor.getState",
14
+ inputSchema: schema(),
15
+ }),
16
+
17
+ bridgeTool({
18
+ name: "creavit_editor_get_settings",
19
+ command: "editor.getSettings",
20
+ description:
21
+ "Reads player settings: background (type/color/image/blur/gradient), padding, radius, shadow, camera settings, cursor settings, zoom ranges, CRT effect and more. Without `keys` it returns EVERYTHING (very large) — read selectively.",
22
+ inputSchema: schema({
23
+ keys: S.array(
24
+ "Return only these settings. e.g. ['padding','radius','backgroundType','backgroundColor','zoomRanges','cameraSettings']",
25
+ { type: "string" },
26
+ ),
27
+ }),
28
+ }),
29
+
30
+ bridgeTool({
31
+ name: "creavit_editor_set_settings",
32
+ command: "editor.setSettings",
33
+ description:
34
+ 'Changes player settings and refreshes the canvas. Partial update: only the keys you pass change. e.g. {"padding": 80, "radius": 24, "backgroundType": "gradient"}. Discover valid key names with creavit_editor_get_settings. The response echoes the values read back after writing — check `verified`. Call creavit_project_save afterwards to persist.',
35
+ inputSchema: schema(
36
+ {
37
+ settings: S.object(
38
+ "Settings to change. Key names must match creavit_editor_get_settings output.",
39
+ ),
40
+ },
41
+ ["settings"],
42
+ ),
43
+ mapResult: (result) =>
44
+ textResult({
45
+ ...result,
46
+ note: result?.rejected?.length
47
+ ? "Some keys could not be written — confirm the exact name with creavit_editor_get_settings."
48
+ : "Applied. Call creavit_project_save to persist.",
49
+ }),
50
+ }),
51
+
52
+ bridgeTool({
53
+ name: "creavit_editor_list_zooms",
54
+ command: "editor.listZooms",
55
+ description:
56
+ "Lists zoom ranges on the timeline: id, start/end (seconds), scale, focus x/y (normalized 0-1).",
57
+ inputSchema: schema(),
58
+ }),
59
+
60
+ bridgeTool({
61
+ name: "creavit_editor_add_zoom",
62
+ command: "editor.addZoom",
63
+ description:
64
+ "Adds a zoom range. start/end are in seconds and end must be greater than start. scale defaults to 2 (1 means no zoom), x/y default to 0.5 (center). Get the video duration from creavit_editor_state.",
65
+ inputSchema: schema(
66
+ {
67
+ start: S.number("Start time in seconds"),
68
+ end: S.number("End time in seconds, greater than start"),
69
+ scale: S.number("Zoom factor, typically 1.5-3 (default 2)"),
70
+ x: S.number("Horizontal focus 0-1 (default 0.5)"),
71
+ y: S.number("Vertical focus 0-1 (default 0.5)"),
72
+ },
73
+ ["start", "end"],
74
+ ),
75
+ }),
76
+
77
+ bridgeTool({
78
+ name: "creavit_editor_update_zoom",
79
+ command: "editor.updateZoom",
80
+ description:
81
+ "Updates an existing zoom range. Target it by id (from creavit_editor_list_zooms) or by index.",
82
+ inputSchema: schema(
83
+ {
84
+ id: S.string("ID of the zoom range"),
85
+ index: S.number("Or its position in the list (0-based)"),
86
+ patch: S.object("Fields to change: start, end, scale, x, y"),
87
+ },
88
+ ["patch"],
89
+ ),
90
+ }),
91
+
92
+ bridgeTool({
93
+ name: "creavit_editor_remove_zoom",
94
+ command: "editor.removeZoom",
95
+ description: "Deletes a zoom range, targeted by id or index.",
96
+ inputSchema: schema({
97
+ id: S.string("ID of the zoom range"),
98
+ index: S.number("Or its position in the list (0-based)"),
99
+ }),
100
+ }),
101
+
102
+ bridgeTool({
103
+ name: "creavit_editor_segments",
104
+ command: "editor.listSegments",
105
+ description:
106
+ "Lists timeline segments (cuts/trims). To edit them, read this output, modify it, and write it back with creavit_editor_set_segments.",
107
+ inputSchema: schema(),
108
+ }),
109
+
110
+ bridgeTool({
111
+ name: "creavit_editor_set_segments",
112
+ command: "editor.setSegments",
113
+ description:
114
+ "Replaces all timeline segments at once (cutting, trimming, reordering). Read the current structure with creavit_editor_segments FIRST — a malformed list can make the project unplayable.",
115
+ inputSchema: schema(
116
+ { segments: S.array("The complete segment list", { type: "object" }) },
117
+ ["segments"],
118
+ ),
119
+ }),
120
+
121
+ bridgeTool({
122
+ name: "creavit_editor_seek",
123
+ command: "editor.seek",
124
+ description:
125
+ "Moves the playhead to the given time and renders that frame. Combine with creavit_editor_screenshot to inspect a specific moment.",
126
+ inputSchema: schema({ time: S.number("Target time in seconds") }, ["time"]),
127
+ }),
128
+
129
+ {
130
+ name: "creavit_editor_playback",
131
+ description: "Starts or pauses preview playback.",
132
+ inputSchema: schema(
133
+ { action: S.string("play or pause", { enum: ["play", "pause"] }) },
134
+ ["action"],
135
+ ),
136
+ // Komut adı argümana göre değiştiği için bridgeTool yerine elle yönlendiriyoruz.
137
+ run: async ({ action }) => {
138
+ if (action !== "play" && action !== "pause") {
139
+ throw new Error("`action` must be either 'play' or 'pause'");
140
+ }
141
+ return textResult(await callCommand(`editor.${action}`, {}));
142
+ },
143
+ },
144
+
145
+ bridgeTool({
146
+ name: "creavit_editor_screenshot",
147
+ command: "editor.screenshot",
148
+ description:
149
+ "Returns the current editor canvas frame as a PNG image. Pass `time` to seek there first. Use this to VERIFY how a change actually looks — always take a look after changing settings.",
150
+ inputSchema: schema({
151
+ time: S.number("Seek to this time in seconds first (optional)"),
152
+ }),
153
+ timeoutMs: 120_000,
154
+ mapResult: (result) => ({
155
+ content: [
156
+ { type: "image", data: result.base64, mimeType: result.mimeType || "image/png" },
157
+ { type: "text", text: `Canvas frame @ ${Number(result.atTime || 0).toFixed(2)}s` },
158
+ ],
159
+ }),
160
+ }),
161
+
162
+ bridgeTool({
163
+ name: "creavit_editor_export",
164
+ command: "editor.export",
165
+ description:
166
+ "Exports the video. This takes a LONG time (minutes). Follow progress with creavit_events (export.started / export.finished / export.failed). Omitted settings fall back to whatever is selected in the editor.",
167
+ inputSchema: schema({
168
+ filePath: S.string("Target file path (.mp4)"),
169
+ format: S.string("mp4 or gif", { enum: ["mp4", "gif"] }),
170
+ resolution: S.string("e.g. 720p, 1080p, 4k"),
171
+ fps: S.number("Frame rate, e.g. 30 or 60"),
172
+ quality: S.string("compact | high | max"),
173
+ }),
174
+ timeoutMs: 3_600_000,
175
+ }),
176
+
177
+ {
178
+ name: "creavit_editor_history",
179
+ description: "Undoes the last editor change, or redoes it.",
180
+ inputSchema: schema(
181
+ { action: S.string("undo or redo", { enum: ["undo", "redo"] }) },
182
+ ["action"],
183
+ ),
184
+ run: async ({ action }) => {
185
+ if (action !== "undo" && action !== "redo") {
186
+ throw new Error("`action` must be either 'undo' or 'redo'");
187
+ }
188
+ return textResult(await callCommand(`editor.${action}`, {}));
189
+ },
190
+ },
191
+ ];
192
+ }
@@ -0,0 +1,112 @@
1
+ // MCP araçları — kaçış kapıları.
2
+ // Tipli araçlar uygulamanın tamamını kapsayamaz; bu araçlar ajanın kalan her
3
+ // şeye ulaşmasını sağlar: komut keşfi, ham IPC ve (izin verilirse) serbest kod.
4
+
5
+ import { schema, S, textResult } from "./defineTool.mjs";
6
+ import { callCommand, listBridgeCommands } from "../bridgeClient.mjs";
7
+
8
+ export function escapeTools() {
9
+ return [
10
+ {
11
+ name: "creavit_capabilities",
12
+ description:
13
+ "Lists EVERY command and editor action the app exposes to agents. When the typed tools (creavit_editor_*, creavit_recording_* …) do not cover what you need, look here and then invoke it with creavit_call. This is the app's real capability surface.",
14
+ inputSchema: schema({
15
+ filter: S.string("Filter by name (substring match, e.g. 'zoom')"),
16
+ includeEditorActions: S.bool(
17
+ "Also fetch actions registered in the editor window (default true)",
18
+ ),
19
+ }),
20
+ run: async ({ filter, includeEditorActions }) => {
21
+ const { commands } = await listBridgeCommands();
22
+ let editorActions = null;
23
+
24
+ if (includeEditorActions !== false) {
25
+ try {
26
+ const result = await callCommand("editor.listActions", {}, 10_000);
27
+ editorActions = result?.actions || null;
28
+ } catch (error) {
29
+ editorActions = { unavailable: error?.message || String(error) };
30
+ }
31
+ }
32
+
33
+ const match = (name) =>
34
+ !filter || String(name).toLowerCase().includes(String(filter).toLowerCase());
35
+
36
+ return textResult({
37
+ commands: commands.filter((c) => match(c.name)),
38
+ editorActions: Array.isArray(editorActions)
39
+ ? editorActions.filter((a) => match(a.name))
40
+ : editorActions,
41
+ howToUse:
42
+ "Invoke a command with creavit_call: {command:'<name>', params:{...}}. " +
43
+ "For entries in editorActions use command='editor.callAction' with " +
44
+ "params={action:'<name>', params:{...}}.",
45
+ });
46
+ },
47
+ },
48
+
49
+ {
50
+ name: "creavit_call",
51
+ description:
52
+ "Invokes any agent command the app exposes — the general-purpose door for anything the typed tools do not cover. Confirm the command name with creavit_capabilities first.",
53
+ inputSchema: schema(
54
+ {
55
+ command: S.string("Command name, e.g. 'editor.getSettings' or 'ipc.invoke'"),
56
+ params: S.object("Command parameters"),
57
+ timeoutMs: S.number("Timeout in milliseconds. Raise it for long operations."),
58
+ },
59
+ ["command"],
60
+ ),
61
+ run: async ({ command, params, timeoutMs }) =>
62
+ textResult(await callCommand(command, params || {}, timeoutMs)),
63
+ },
64
+
65
+ {
66
+ name: "creavit_ipc",
67
+ description:
68
+ "Calls one of the app's raw IPC channels — the lowest level of access: file operations, screen/window queries, settings read-write, FFmpeg operations. Omit `channel` to list every known channel name.",
69
+ inputSchema: schema({
70
+ channel: S.string(
71
+ "IPC channel name, e.g. 'GET_MAC_SCREENS'. Omit to get the list of channels.",
72
+ ),
73
+ args: S.array("Arguments passed to the channel", {}),
74
+ timeoutMs: S.number("Timeout in milliseconds"),
75
+ }),
76
+ run: async ({ channel, args, timeoutMs }) => {
77
+ if (!channel) {
78
+ return textResult(await callCommand("ipc.channels", {}, 10_000));
79
+ }
80
+ return textResult(
81
+ await callCommand("ipc.invoke", { channel, args: args || [] }, timeoutMs || 120_000),
82
+ );
83
+ },
84
+ },
85
+
86
+ {
87
+ name: "creavit_eval",
88
+ description:
89
+ "Runs JavaScript inside an app window — LAST RESORT. Disabled by default; the app must be started with CREAVIT_AGENT_BRIDGE_EVAL=1. Try the typed tools and creavit_capabilities first.",
90
+ inputSchema: schema(
91
+ {
92
+ code: S.string("JavaScript to run. Use `return` to produce a value."),
93
+ windowId: S.number("Target window (defaults to the focused window)"),
94
+ },
95
+ ["code"],
96
+ ),
97
+ run: async ({ code, windowId }) =>
98
+ textResult(await callCommand("debug.eval", { code, windowId }, 60_000)),
99
+ },
100
+
101
+ {
102
+ name: "creavit_logs",
103
+ description:
104
+ "Returns buffered console output from an app window. Use it to diagnose when something behaves unexpectedly.",
105
+ inputSchema: schema({
106
+ limit: S.number("How many lines (default 100)"),
107
+ level: S.string("log | warn | error", { enum: ["log", "warn", "error"] }),
108
+ }),
109
+ run: async (args) => textResult(await callCommand("debug.consoleLogs", args || {}, 15_000)),
110
+ },
111
+ ];
112
+ }
@@ -0,0 +1,53 @@
1
+ // MCP araçları — birleştirme ve çağrı yönlendirme.
2
+
3
+ import { projectTools } from "./projectTools.mjs";
4
+ import { editorTools } from "./editorTools.mjs";
5
+ import { systemTools } from "./systemTools.mjs";
6
+ import { escapeTools } from "./escapeTools.mjs";
7
+ import { errorResult } from "./defineTool.mjs";
8
+ import { BridgeUnavailableError } from "../bridgeClient.mjs";
9
+
10
+ let registry = null;
11
+
12
+ function buildRegistry() {
13
+ if (registry) return registry;
14
+ const all = [...systemTools(), ...projectTools(), ...editorTools(), ...escapeTools()];
15
+
16
+ const byName = new Map();
17
+ for (const tool of all) {
18
+ if (byName.has(tool.name)) {
19
+ throw new Error(`Duplicate tool name: ${tool.name}`);
20
+ }
21
+ byName.set(tool.name, tool);
22
+ }
23
+ registry = byName;
24
+ return registry;
25
+ }
26
+
27
+ export async function listTools() {
28
+ return Array.from(buildRegistry().values()).map(({ name, description, inputSchema }) => ({
29
+ name,
30
+ description,
31
+ inputSchema,
32
+ }));
33
+ }
34
+
35
+ export async function callTool(name, args) {
36
+ const tool = buildRegistry().get(name);
37
+ if (!tool) {
38
+ return errorResult(
39
+ new Error(
40
+ `Unknown tool: ${name}. Available tools: ${Array.from(buildRegistry().keys()).join(", ")}`,
41
+ ),
42
+ );
43
+ }
44
+
45
+ try {
46
+ return await tool.run(args || {});
47
+ } catch (error) {
48
+ // Uygulama kapalıysa ajanın ne yapması gerektiğini net söyle; sessizce
49
+ // "hata" demek yerine yönlendir.
50
+ if (error instanceof BridgeUnavailableError) return errorResult(error);
51
+ return errorResult(error);
52
+ }
53
+ }
@@ -0,0 +1,90 @@
1
+ // MCP araçları — proje (.crvt) işlemleri.
2
+ // Salt-okunur araçlar önce dosya sisteminden okur; uygulama kapalıyken de
3
+ // çalışsın diye. Özetleme daima yerelde yapılır (manifestSummary.mjs).
4
+
5
+ import { bridgeTool, schema, S, textResult } from "./defineTool.mjs";
6
+ import { callCommand } from "../bridgeClient.mjs";
7
+ import { canReadOffline, listProjectsOffline, readManifestOffline } from "../crvtReader.mjs";
8
+ import { summarizeManifest } from "../manifestSummary.mjs";
9
+
10
+ // Manifest'i önce yerelden, olmazsa çalışan uygulamadan al.
11
+ async function loadManifest(filePath) {
12
+ if (canReadOffline()) {
13
+ try {
14
+ return await readManifestOffline(filePath);
15
+ } catch (error) {
16
+ // Dosya yoksa köprü de bulamaz; boşuna tur atma.
17
+ if (String(error?.message || "").includes("ENOENT")) throw error;
18
+ }
19
+ }
20
+ const result = await callCommand("project.readManifest", { filePath });
21
+ return result?.manifest;
22
+ }
23
+
24
+ export function projectTools() {
25
+ return [
26
+ {
27
+ name: "creavit_project_list",
28
+ description:
29
+ "Lists Creavit Studio projects (.crvt): name, file path, duration, size, modified date. Newest first. Works even when the app is closed. Start here — take the file path for every other project tool from this list.",
30
+ inputSchema: schema({
31
+ dir: S.string("Folder to scan. Defaults to ~/Downloads/Creavit Studio"),
32
+ limit: S.number("Maximum number of projects to return (default 100)"),
33
+ }),
34
+ run: async (args) => {
35
+ if (canReadOffline()) return textResult(await listProjectsOffline(args));
36
+ return textResult(await callCommand("project.list", args));
37
+ },
38
+ },
39
+
40
+ {
41
+ name: "creavit_project_summary",
42
+ description:
43
+ "Readable summary of a .crvt project: duration, resolution, background, clip/zoom/segment counts, active effects, media paths. Works even when the app is closed. Call THIS first to understand a project — the full manifest is very large.",
44
+ inputSchema: schema({ filePath: S.string("Absolute path to the .crvt file") }, ["filePath"]),
45
+ run: async ({ filePath }) =>
46
+ textResult(summarizeManifest(await loadManifest(filePath), filePath)),
47
+ },
48
+
49
+ {
50
+ name: "creavit_project_manifest",
51
+ description:
52
+ "Returns the FULL manifest.json of a .crvt project (all player settings, zoom ranges, clips, segments, cursor data). Works even when the app is closed. Can be very large — try creavit_project_summary first.",
53
+ inputSchema: schema({ filePath: S.string("Absolute path to the .crvt file") }, ["filePath"]),
54
+ run: async ({ filePath }) =>
55
+ textResult({ filePath, manifest: await loadManifest(filePath) }),
56
+ },
57
+
58
+ bridgeTool({
59
+ name: "creavit_project_open",
60
+ command: "project.open",
61
+ description:
62
+ "Opens a .crvt project in the app's editor. Required before any creavit_editor_* tool will work. Loading takes a few seconds — afterwards poll creavit_editor_state until duration is greater than 0 before making changes, otherwise your edits get overwritten when loading finishes.",
63
+ inputSchema: schema({ filePath: S.string("Absolute path to the .crvt file") }, ["filePath"]),
64
+ timeoutMs: 120_000,
65
+ }),
66
+
67
+ bridgeTool({
68
+ name: "creavit_project_reveal",
69
+ command: "project.reveal",
70
+ description: "Reveals a file in macOS Finder.",
71
+ inputSchema: schema({ filePath: S.string("Absolute path to the file") }, ["filePath"]),
72
+ }),
73
+
74
+ bridgeTool({
75
+ name: "creavit_project_save",
76
+ command: "editor.saveProject",
77
+ description:
78
+ "Saves the project currently open in the editor. Pass filePath to save as a copy, omit it to overwrite the current file. Call this after changing settings to make them permanent.",
79
+ inputSchema: schema({
80
+ filePath: S.string("Target .crvt path (omit to overwrite in place)"),
81
+ }),
82
+ timeoutMs: 300_000,
83
+ mapResult: (result) =>
84
+ textResult({
85
+ ...result,
86
+ note: "Save complete. Verify what landed on disk with creavit_project_summary.",
87
+ }),
88
+ }),
89
+ ];
90
+ }