creavit-studio-mcp 1.3.1 → 1.3.3

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 CHANGED
@@ -111,9 +111,15 @@ files directly and work with the app closed.
111
111
  `creavit_editor_export`, `creavit_editor_history`
112
112
 
113
113
  **Appearance** — `creavit_editor_background_get`,
114
- `creavit_editor_background_set`, `creavit_editor_camera_get`,
114
+ `creavit_editor_background_set`, `creavit_editor_wallpapers`,
115
+ `creavit_editor_wallpaper_set`, `creavit_editor_camera_get`,
115
116
  `creavit_editor_camera_set`, `creavit_editor_audio`
116
117
 
118
+ > Zoom ranges are normally **not** something you add. Creavit Studio records the
119
+ > real mouse clicks and builds its own auto-zoom segments in the editor; use
120
+ > `creavit_editor_add_zoom` only when the user asks for a zoom at a specific
121
+ > moment.
122
+
117
123
  **Perspective** — `creavit_editor_perspectives`,
118
124
  `creavit_editor_add_perspective`, `creavit_editor_update_perspective`,
119
125
  `creavit_editor_remove_perspective`
@@ -175,6 +181,9 @@ Use the typed tools instead of guessing generic setting keys:
175
181
  ```text
176
182
  creavit_editor_background_set type=solid color=#10131a
177
183
  creavit_editor_background_set type=gradient gradient={type, direction, colors}
184
+ creavit_editor_wallpapers categories + sample names
185
+ creavit_editor_wallpapers category=macos that category's full list
186
+ creavit_editor_wallpaper_set name=sonoma-dark ready-made CMS wallpaper
178
187
  creavit_editor_camera_set settings={visible,size,radius,shadow,mirror,...}
179
188
  creavit_editor_audio action=mute
180
189
  creavit_project_save
@@ -204,14 +213,22 @@ composited in.
204
213
 
205
214
  ### Record a URL walkthrough
206
215
 
207
- `creavit_record_url_walkthrough` is the high-level workflow. By default it opens
216
+ `creavit_record_url_walkthrough` is the high-level, atomic workflow. Use one call
217
+ instead of separate browser/start/interact/stop calls so client round trips and
218
+ retries cannot create gaps or duplicate recordings. By default it opens
208
219
  the URL in a frameless site-only capture window (`16:9`, `4:3`, `1:1`, `9:16`,
209
220
  or another requested ratio) and waits for the DOM, fonts, and images before
210
221
  recording. It then executes ordered semantic actions with the real macOS mouse,
211
222
  so the normal Creavit custom-cursor track receives authentic move/click events.
212
- It trims the timeline to `durationMs`, adds
213
- scroll-highlight zooms, optionally covers the whole video with a perspective
214
- preset, mutes it, and saves it.
223
+ It trims the timeline to `durationMs`, optionally covers the whole video with a
224
+ perspective preset, mutes it, and saves it. With `export: true`, the tool starts
225
+ the render in the background and returns promptly; `openExport: true` opens the
226
+ video automatically when rendering finishes, without requiring export UI.
227
+
228
+ It does **not** add zoom ranges: the app records the real clicks and generates
229
+ its own auto-zoom segments in the editor, which land on the actual click times.
230
+ `autoZoom: true` (plus `replaceZooms: true` to wipe the app's own ones) is
231
+ opt-in, for when the user explicitly wants scripted zooms.
215
232
 
216
233
  When the calling agent has inspected the page, it can pass ordered `steps`
217
234
  (`scrollPages`, `dwellMs`, `zoom`, `scale`, `x`, `y`, and optionally
@@ -232,11 +249,13 @@ Example input:
232
249
  { "action": "click", "text": "Get Started", "dwellMs": 3000 }
233
250
  ],
234
251
  "controlSystemMouse": true,
235
- "autoZoom": true,
236
- "zoomScale": 1.8,
237
252
  "perspective": true,
238
253
  "perspectiveTemplate": "minimal-tilt",
239
- "mute": true
254
+ "mute": true,
255
+ "export": true,
256
+ "exportResolution": "1080p",
257
+ "exportFps": 60,
258
+ "openExport": true
240
259
  }
241
260
  ```
242
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "creavit-studio-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "MCP server that lets AI coding agents (Claude Code, Codex, Cursor) drive the Creavit Studio screen recording app",
5
5
  "keywords": [
6
6
  "mcp",
package/src/protocol.mjs CHANGED
@@ -6,7 +6,7 @@ export const ENDPOINT_FILENAME = "agent-bridge.json";
6
6
 
7
7
  export const MCP_PROTOCOL_VERSION = "2024-11-05";
8
8
  export const SERVER_NAME = "creavit-studio";
9
- export const SERVER_VERSION = "1.2.0";
9
+ export const SERVER_VERSION = "1.3.2";
10
10
 
11
11
  export const JSONRPC_ERRORS = {
12
12
  PARSE_ERROR: -32700,
package/src/rpcServer.mjs CHANGED
@@ -40,6 +40,8 @@ export function startRpcServer({ listTools, callTool }) {
40
40
  protocolVersion: MCP_PROTOCOL_VERSION,
41
41
  capabilities: { tools: { listChanged: false } },
42
42
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
43
+ instructions:
44
+ "Plan the complete Creavit tool sequence before the first call, then execute it without unnecessary pauses. For any website workflow that includes open + record + multiple interactions + stop, use creavit_record_url_walkthrough once instead of separate browser/recording calls; it can also start export in the background and open the result when ready. Mutating recording/export calls are idempotent, so do not stop/restart merely because a client-side timeout made the result uncertain—check creavit_recording_status or creavit_events.",
43
45
  };
44
46
  },
45
47
 
@@ -135,6 +135,9 @@ function trimSegmentsToDuration(segments, targetDuration) {
135
135
  }
136
136
 
137
137
  export function browserTools() {
138
+ let activeUrlWalkthroughPromise = null;
139
+ let recentUrlWalkthrough = null;
140
+
138
141
  return [
139
142
  bridgeTool({
140
143
  name: "creavit_browser_open",
@@ -165,6 +168,17 @@ export function browserTools() {
165
168
  ),
166
169
  }),
167
170
 
171
+ bridgeTool({
172
+ name: "creavit_browser_activate",
173
+ command: "browser.activate",
174
+ description:
175
+ "Brings the capture window or external browser to the foreground without changing its size or entering fullscreen. Call immediately after recording starts if you are driving the recording manually.",
176
+ inputSchema: schema({
177
+ captureWindowId: S.number("Clean capture window ID"),
178
+ browser: S.string("External browser name when there is no captureWindowId"),
179
+ }),
180
+ }),
181
+
168
182
  bridgeTool({
169
183
  name: "creavit_browser_focus",
170
184
  command: "browser.focusPoint",
@@ -220,7 +234,7 @@ export function browserTools() {
220
234
  {
221
235
  name: "creavit_record_url_walkthrough",
222
236
  description:
223
- "Records a site in a clean configurable-ratio window after real page readiness. It can execute ordered semantic steps using the real system mouse, trims to an exact duration, adds zooms, optional perspective, and optionally mutes it.",
237
+ "USE THIS SINGLE ATOMIC TOOL for requests that open a website, record several clicks/types, stop, and optionally export/open the video. Do not split that workflow into manual browser + recording calls: this tool prevents duplicate recordings and agent round-trip gaps. It opens a non-fullscreen configurable-ratio window, waits for real page readiness, keeps camera off, defaults microphone/system audio off, executes ordered semantic steps, and can export/open the final video. Zooms are NOT added: Creavit Studio records the real clicks and generates its own auto-zoom segments in the editor. Only pass autoZoom=true when the user explicitly asks for extra scripted zooms.",
224
238
  inputSchema: schema(
225
239
  {
226
240
  url: S.string("http(s) page to record"),
@@ -240,16 +254,33 @@ export function browserTools() {
240
254
  tailMs: S.number("Still time after the last scroll (default 800)"),
241
255
  controlSystemMouse: S.bool("Move/click the real macOS mouse so Creavit records native cursor data (default true)"),
242
256
  mute: S.bool("Disable recording audio and mute export clips (default true)"),
243
- autoZoom: S.bool("Add zooms around section destinations (default true)"),
244
- replaceZooms: S.bool("Remove recorder-created zooms first (default true)"),
257
+ autoZoom: S.bool("Add scripted zooms around step destinations (default FALSE — the app already creates zooms from the real clicks; only enable when the user asks for it)"),
258
+ replaceZooms: S.bool("Delete the app's own click-based zooms before adding scripted ones (default false; only meaningful with autoZoom=true)"),
245
259
  zoomScale: S.number("Automatic zoom scale (default 1.7)"),
246
260
  perspective: S.bool("Apply perspective to the complete result"),
247
261
  perspectiveTemplate: S.string("Perspective template id (default minimal-tilt)"),
248
262
  save: S.bool("Save the generated project after editing (default true)"),
263
+ export: S.bool("Start export in the background after recording/editing (default false)"),
264
+ exportFilePath: S.string("Optional absolute target .mp4 path; defaults beside the .crvt project"),
265
+ exportResolution: S.string("Export resolution (default 1080p)"),
266
+ exportFps: S.number("Export frame rate (default 60)"),
267
+ exportQuality: S.string("Export quality (default high)", {
268
+ enum: ["compact", "high", "max"],
269
+ }),
270
+ openExport: S.bool("Open the exported video when finished (default true when export=true)"),
249
271
  },
250
272
  ["url"],
251
273
  ),
252
- run: async (args) => {
274
+ run: (args) => {
275
+ const requestKey = JSON.stringify(args || {});
276
+ if (
277
+ recentUrlWalkthrough?.key === requestKey &&
278
+ Date.now() < recentUrlWalkthrough.expiresAt
279
+ ) {
280
+ return Promise.resolve(recentUrlWalkthrough.result);
281
+ }
282
+ if (activeUrlWalkthroughPromise) return activeUrlWalkthroughPromise;
283
+ const operation = (async () => {
253
284
  const browser = canonicalBrowser(args.browser || "Zen Browser");
254
285
  const cleanWindow = args.cleanWindow !== false;
255
286
  const loadWaitMs = Math.max(0, Math.min(60_000, Number(args.loadWaitMs) || 1500));
@@ -259,8 +290,12 @@ export function browserTools() {
259
290
  const scrollIntervalMs = Math.max(300, Math.min(15_000, Number(args.scrollIntervalMs) || 1400));
260
291
  const tailMs = Math.max(200, Math.min(30_000, Number(args.tailMs) || 800));
261
292
  const muted = args.mute !== false;
262
- const autoZoom = args.autoZoom !== false;
263
- const replaceZooms = args.replaceZooms !== false;
293
+ // Zoom'ları KENDİMİZ eklemiyoruz: Creavit Studio gerçek tıklamaları
294
+ // kaydedip editörde otomatik zoom segmentlerini kendi üretiyor. Bizim
295
+ // tahmini zamanlamayla eklediğimiz zoom'lar onların üstüne yanlış oturuyordu.
296
+ // Sadece kullanıcı açıkça isterse (autoZoom=true) devreye girer.
297
+ const autoZoom = args.autoZoom === true;
298
+ const replaceZooms = autoZoom && args.replaceZooms === true;
264
299
  const controlSystemMouse = args.controlSystemMouse !== false;
265
300
  const zoomScale = Math.max(1, Math.min(5, Number(args.zoomScale) || 1.7));
266
301
  const requestedSteps = Array.isArray(args.steps) && args.steps.length ? args.steps : args.sections;
@@ -325,7 +360,7 @@ export function browserTools() {
325
360
  }
326
361
  const browserWindow = await waitForBrowserWindow(captureWindowId ? "Creavit Studio" : browser, titleHint, 20_000);
327
362
 
328
- await callCommand("recording.start", {
363
+ const startResult = await callCommand("recording.start", {
329
364
  options: {
330
365
  startScreen: true,
331
366
  cameraEnabled: false,
@@ -334,7 +369,19 @@ export function browserTools() {
334
369
  recordingSource: { type: "window", windowId: browserWindow.id, windowInfo: browserWindow },
335
370
  },
336
371
  }, 180_000);
372
+ if (startResult?.started === false) {
373
+ throw new Error(
374
+ "A recording is already active. Stop it before starting a URL walkthrough.",
375
+ );
376
+ }
337
377
  recordingStarted = true;
378
+ // recording.start briefly focuses the recorder controls. Restore the
379
+ // requested browser immediately so the actual walkthrough is visibly
380
+ // in front from the first recorded frame onward.
381
+ await callCommand("browser.activate", {
382
+ captureWindowId,
383
+ browser: captureWindowId ? undefined : browser,
384
+ });
338
385
  const recordingStartedAt = Date.now();
339
386
  const contentDeadline = durationMs ? recordingStartedAt + durationMs - tailMs : Number.POSITIVE_INFINITY;
340
387
  await sleep(Math.min(leadInMs, Math.max(0, contentDeadline - Date.now())));
@@ -351,6 +398,7 @@ export function browserTools() {
351
398
  selector: section.selector,
352
399
  text: section.text,
353
400
  value: section.value,
401
+ useSystemCursor: controlSystemMouse,
354
402
  }, 30_000);
355
403
  if (Number.isFinite(interaction?.x)) focus = { x: interaction.x, y: interaction.y };
356
404
  } else {
@@ -383,9 +431,24 @@ export function browserTools() {
383
431
  }
384
432
  await callCommand("recording.stop", {}, 600_000);
385
433
  recordingStarted = false;
434
+ // The source is finalized now. Closing it before editor processing/export
435
+ // avoids leaving a stale capture window over the foreground app.
436
+ if (captureWindowId) {
437
+ await callCommand("browser.close", { captureWindowId });
438
+ captureWindowId = null;
439
+ }
386
440
 
387
441
  const editorState = await waitForEditor(previousEditor?.projectFilePath, 600_000);
388
- const applied = { muted: false, zooms: [], perspective: null, saved: false, trimmedTo: null };
442
+ const applied = {
443
+ muted: false,
444
+ zooms: [],
445
+ zoomSource: autoZoom ? "scripted (autoZoom=true)" : "editor auto-zoom from the recorded clicks",
446
+ perspective: null,
447
+ saved: false,
448
+ trimmedTo: null,
449
+ export: null,
450
+ openedExport: null,
451
+ };
389
452
  let finalDuration = Number(editorState.duration);
390
453
  if (durationMs) {
391
454
  const targetDuration = durationMs / 1000;
@@ -421,7 +484,24 @@ export function browserTools() {
421
484
  await callCommand("editor.saveProject", {}, 180_000);
422
485
  applied.saved = true;
423
486
  }
424
- return textResult({ ok: true, url: args.url, browser: cleanWindow ? "Creavit Capture" : browser, cleanWindow, page: openResult, window: browserWindow, duration: finalDuration, rawDuration: editorState.duration, projectFilePath: editorState.projectFilePath, applied });
487
+ if (args.export === true) {
488
+ const exportResult = await callCommand(
489
+ "editor.export.start",
490
+ {
491
+ ...(args.exportFilePath ? { filePath: args.exportFilePath } : {}),
492
+ format: "mp4",
493
+ resolution: args.exportResolution || "1080p",
494
+ fps: Math.max(1, Math.min(120, Math.round(Number(args.exportFps) || 60))),
495
+ quality: args.exportQuality || "high",
496
+ openWhenFinished: args.openExport !== false,
497
+ },
498
+ 30_000,
499
+ );
500
+ applied.export = exportResult;
501
+ applied.openedExport =
502
+ args.openExport !== false ? "scheduled when export finishes" : null;
503
+ }
504
+ return textResult({ ok: true, url: args.url, browser: cleanWindow ? "Creavit Capture" : browser, cleanWindow, page: openResult, window: browserWindow, duration: finalDuration, rawDuration: editorState.duration, projectFilePath: editorState.projectFilePath, exportStartedInBackground: applied.export?.started === true, exportJob: applied.export?.job || null, applied });
425
505
  } finally {
426
506
  if (recordingStarted) {
427
507
  try { await callCommand("recording.stop", {}, 600_000); } catch (_) {}
@@ -430,6 +510,20 @@ export function browserTools() {
430
510
  try { await callCommand("browser.close", { captureWindowId }); } catch (_) {}
431
511
  }
432
512
  }
513
+ })();
514
+ activeUrlWalkthroughPromise = operation;
515
+ return operation.then((result) => {
516
+ recentUrlWalkthrough = {
517
+ key: requestKey,
518
+ result,
519
+ expiresAt: Date.now() + 60_000,
520
+ };
521
+ return result;
522
+ }).finally(() => {
523
+ if (activeUrlWalkthroughPromise === operation) {
524
+ activeUrlWalkthroughPromise = null;
525
+ }
526
+ });
433
527
  },
434
528
  },
435
529
  ];
@@ -61,14 +61,15 @@ export function editorTools() {
61
61
  name: "creavit_editor_background_set",
62
62
  command: "editor.setBackground",
63
63
  description:
64
- "Sets the canvas background. For solid use type='solid' and color. For gradient pass at least two stops as {type:'linear'|'radial', direction:'to-right', colors:[{color:'#...',position:0},...]}. Call creavit_project_save afterwards.",
64
+ "Sets the canvas background. For solid use type='solid' and color. For gradient pass at least two stops as {type:'linear'|'radial', direction:'to-right', colors:[{color:'#...',position:0},...]}. For a ready-made wallpaper use type='image' with `wallpaper` (name from creavit_editor_wallpapers) — or use creavit_editor_wallpaper_set directly. Call creavit_project_save afterwards.",
65
65
  inputSchema: schema(
66
66
  {
67
67
  type: S.string("Background type", {
68
68
  enum: ["solid", "color", "gradient", "image", "dynamic"],
69
69
  }),
70
70
  color: S.string("CSS color for a solid background"),
71
- image: S.string("Image URL or local path"),
71
+ image: S.string("Image URL, local path, or a CMS wallpaper name"),
72
+ wallpaper: S.string("CMS wallpaper name (see creavit_editor_wallpapers)"),
72
73
  blur: S.number("Image blur amount"),
73
74
  gradient: S.object(
74
75
  "Gradient configuration: {type, direction, colors:[{color,position}]}.",
@@ -81,6 +82,35 @@ export function editorTools() {
81
82
  textResult({ ...result, note: "Applied. Call creavit_project_save to persist." }),
82
83
  }),
83
84
 
85
+ bridgeTool({
86
+ name: "creavit_editor_wallpapers",
87
+ command: "editor.listWallpapers",
88
+ description:
89
+ "Lists the ready-made wallpapers (background images from the Creavit CMS) grouped by category. Without arguments it returns the categories with a small sample of names — pass `category` for that category's complete list, or `search` to look a name up. Feed the name to creavit_editor_wallpaper_set.",
90
+ inputSchema: schema({
91
+ category: S.string("Only this category, e.g. 'macos'"),
92
+ search: S.string("Find wallpapers whose name contains this text"),
93
+ limit: S.number("Maximum number of names to return"),
94
+ }),
95
+ }),
96
+
97
+ bridgeTool({
98
+ name: "creavit_editor_wallpaper_set",
99
+ command: "editor.setWallpaper",
100
+ description:
101
+ "Sets the canvas background to a CMS wallpaper — exactly what picking one in the UI does. Select it by `name` (from creavit_editor_wallpapers), by `category` + `index`, or with random=true. Call creavit_project_save afterwards.",
102
+ inputSchema: schema({
103
+ name: S.string("Wallpaper name, e.g. 'sonoma-dark'"),
104
+ category: S.string("Category to pick from, e.g. 'macos'"),
105
+ index: S.number("Zero-based position inside the category"),
106
+ random: S.bool("Pick a random wallpaper"),
107
+ blur: S.number("Background blur amount (optional)"),
108
+ }),
109
+ timeoutMs: 60_000,
110
+ mapResult: (result) =>
111
+ textResult({ ...result, note: "Applied. Call creavit_project_save to persist." }),
112
+ }),
113
+
84
114
  bridgeTool({
85
115
  name: "creavit_editor_camera_get",
86
116
  command: "editor.getCamera",
@@ -136,7 +166,7 @@ export function editorTools() {
136
166
  name: "creavit_editor_add_zoom",
137
167
  command: "editor.addZoom",
138
168
  description:
139
- "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.",
169
+ "Adds a zoom range MANUALLY. Do NOT use this to 'zoom on the clicks' after a recording — Creavit Studio records the real mouse clicks and builds its own auto-zoom segments in the editor, and hand-added ranges land on estimated times instead. Only call it when the user explicitly asks for a zoom at a specific moment. 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.",
140
170
  inputSchema: schema(
141
171
  {
142
172
  start: S.number("Start time in seconds"),
@@ -284,7 +314,7 @@ export function editorTools() {
284
314
  name: "creavit_editor_export",
285
315
  command: "editor.export",
286
316
  description:
287
- "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.",
317
+ "Exports the video idempotently; concurrent retries share one render. This takes a LONG time (minutes). Follow progress with creavit_events (export.started / export.finished / export.failed), then use creavit_file_open when the user asks to open/play it. If filePath is omitted, the export is written beside the open .crvt project.",
288
318
  inputSchema: schema({
289
319
  filePath: S.string("Target file path (.mp4)"),
290
320
  format: S.string("mp4 or gif", { enum: ["mp4", "gif"] }),
@@ -295,6 +325,22 @@ export function editorTools() {
295
325
  timeoutMs: 3_600_000,
296
326
  }),
297
327
 
328
+ bridgeTool({
329
+ name: "creavit_editor_export_start",
330
+ command: "editor.export.start",
331
+ description:
332
+ "Starts one export in the background and returns immediately. Use this when tool latency matters. Progress/completion is reported by creavit_events; set openWhenFinished=true to open/play the result automatically without showing export UI.",
333
+ inputSchema: schema({
334
+ filePath: S.string("Target file path (.mp4); omit to export beside the project"),
335
+ format: S.string("mp4 or gif", { enum: ["mp4", "gif"] }),
336
+ resolution: S.string("e.g. 720p, 1080p, 4k"),
337
+ fps: S.number("Frame rate, e.g. 30 or 60"),
338
+ quality: S.string("compact | high | max"),
339
+ openWhenFinished: S.bool("Open the exported file automatically when rendering completes"),
340
+ }),
341
+ timeoutMs: 30_000,
342
+ }),
343
+
298
344
  {
299
345
  name: "creavit_editor_history",
300
346
  description: "Undoes the last editor change, or redoes it.",
@@ -71,6 +71,14 @@ export function projectTools() {
71
71
  inputSchema: schema({ filePath: S.string("Absolute path to the file") }, ["filePath"]),
72
72
  }),
73
73
 
74
+ bridgeTool({
75
+ name: "creavit_file_open",
76
+ command: "project.openFile",
77
+ description:
78
+ "Opens an existing exported video or other file with its default macOS application. Use this after creavit_editor_export when the user asks to open/play the result.",
79
+ inputSchema: schema({ filePath: S.string("Absolute path to the file") }, ["filePath"]),
80
+ }),
81
+
74
82
  bridgeTool({
75
83
  name: "creavit_project_save",
76
84
  command: "editor.saveProject",
@@ -74,7 +74,7 @@ export function systemTools() {
74
74
  name: "creavit_recording_start",
75
75
  command: "recording.start",
76
76
  description:
77
- "Starts a screen recording. Get source IDs from creavit_devices_list first. Monitor with creavit_recording_status, and finish with creavit_recording_stop.",
77
+ "Starts a screen recording idempotently: retries while starting share the same native operation, and calling it while active does not restart. Get source IDs from creavit_devices_list first. For a multi-step website recording, use ONE creavit_record_url_walkthrough call instead of this manual tool.",
78
78
  inputSchema: schema({
79
79
  options: S.object(
80
80
  "Recording options: {sourceType:'display'|'window'|'area', sourceId, cameraEnabled, micEnabled, systemAudioEnabled, delayMs, area:{x,y,width,height}}. cameraEnabled:false also turns the camera off and hides its window, so it does not appear in a screen recording; cameraEnabled:true opens and starts it.",
@@ -88,7 +88,7 @@ export function systemTools() {
88
88
  name: "creavit_recording_stop",
89
89
  command: "recording.stop",
90
90
  description:
91
- "Stops the recording. Processing can take a few minutes; the editor opens automatically when it finishes. Track progress with creavit_events (watch for recording.stopped and project.loaded).",
91
+ "Stops the recording idempotently. Processing can take a few minutes; concurrent retries share the same operation and the editor opens automatically when it finishes. Track progress with creavit_events (watch for recording.stopped and project.loaded).",
92
92
  inputSchema: schema(),
93
93
  timeoutMs: 600_000,
94
94
  }),