@writepanda/mcp 1.251.0 → 1.262.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/bin/server.mjs CHANGED
@@ -35,7 +35,13 @@ import path from "node:path";
35
35
  import process from "node:process";
36
36
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
37
37
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
38
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
38
+ import {
39
+ CallToolRequestSchema,
40
+ ListResourcesRequestSchema,
41
+ ListResourceTemplatesRequestSchema,
42
+ ListToolsRequestSchema,
43
+ ReadResourceRequestSchema,
44
+ } from "@modelcontextprotocol/sdk/types.js";
39
45
  import {
40
46
  appLaunchEnv,
41
47
  callWithReconnect,
@@ -47,6 +53,14 @@ import {
47
53
  IN_APP_UNAVAILABLE,
48
54
  isInAppCaller,
49
55
  } from "./appConnection.mjs";
56
+ import {
57
+ APP_TOOLS,
58
+ clientSupportsUi,
59
+ listUiResources,
60
+ MediaRegistry,
61
+ readUiResource,
62
+ toolUiMeta,
63
+ } from "./apps/index.mjs";
50
64
  import { portableSchema } from "./portableSchema.mjs";
51
65
 
52
66
  // ── Credentials + transport ───────────────────────────────────────────
@@ -1274,6 +1288,29 @@ const TOOLS = [
1274
1288
  borderRadius: { type: "number" },
1275
1289
  showBlur: { type: "boolean" },
1276
1290
  motionBlurAmount: { type: "number" },
1291
+ wallpaperMotion: { type: "number", description: "mesh: background speed 0-2." },
1292
+ screenFrame: {
1293
+ type: "object",
1294
+ description: "Device frame for screen recordings.",
1295
+ properties: {
1296
+ kind: {
1297
+ type: "string",
1298
+ enum: ["browser", "window", "laptop", "phone", "tablet"],
1299
+ },
1300
+ theme: { type: "string", enum: ["light", "dark"] },
1301
+ url: { type: "string" },
1302
+ },
1303
+ },
1304
+ screenTilt: {
1305
+ type: "object",
1306
+ description: "3D tilt of screen recordings.",
1307
+ properties: {
1308
+ angle: { type: "string", enum: ["flat", "left", "right", "above"] },
1309
+ followCursor: { type: "boolean" },
1310
+ clickBounce: { type: "boolean" },
1311
+ strength: { type: "number", description: "0-1." },
1312
+ },
1313
+ },
1277
1314
  },
1278
1315
  },
1279
1316
  graphicSound: {
@@ -1682,6 +1719,19 @@ const TOOLS = [
1682
1719
  },
1683
1720
  command: "project.show",
1684
1721
  },
1722
+ {
1723
+ name: "project_edit_summary",
1724
+ description:
1725
+ "What the edit contains now: lengths (editedDurationMs, sourceDurationMs, removedMs cut or sped up), counts (cuts, zooms, motionElements, graphics, transitions, brollBeats, mediaOverlays, captions { on, template }, audio { narration, music, soundEffects }, ...) and highlights[] (plain lines). Cheap; use it to report an editing pass instead of project_read.",
1726
+ inputSchema: {
1727
+ type: "object",
1728
+ properties: {
1729
+ id: { type: "string" },
1730
+ path: { type: "string" },
1731
+ },
1732
+ },
1733
+ command: "project.edit-summary",
1734
+ },
1685
1735
  {
1686
1736
  name: "matte_status",
1687
1737
  description:
@@ -2412,12 +2462,18 @@ const TOOLS = [
2412
2462
  {
2413
2463
  name: "podcast_draft_rundown",
2414
2464
  description:
2415
- "Have the studio's producer draft the run of show from a short brief: segments with questions, guest lower thirds, and a YouTube title, description and thumbnail idea, on the model the user picked for the assistant. Replaces the current run of show unless apply=false.",
2465
+ "Have the studio's producer draft the run of show from a short brief: segments with questions, guest lower thirds, and a YouTube title, description and thumbnail idea, on the model the user picked for the assistant. Pass the host's own questions as questions: each goes in word for word (on the prompter card) with a lower third. Replaces the current run of show unless apply=false.",
2416
2466
  inputSchema: {
2417
2467
  type: "object",
2418
2468
  properties: {
2419
2469
  brief: { type: "string" },
2420
2470
  minutes: { type: "number", description: "Total show length (default 45)." },
2471
+ questions: {
2472
+ type: "array",
2473
+ items: { type: "string" },
2474
+ description:
2475
+ "The host's own questions, in order: kept word for word, each with a lower third.",
2476
+ },
2421
2477
  guests: {
2422
2478
  type: "array",
2423
2479
  items: {
@@ -3016,7 +3072,7 @@ const TOOLS = [
3016
3072
  type: "boolean",
3017
3073
  description: "Follow the recorded cursor (screen recordings).",
3018
3074
  },
3019
- soundUrl: { type: "string", description: 'Default bundled:sound/swoosh-fast; "none".' },
3075
+ soundUrl: { type: "string", description: 'Default bundled:sound/motion-swoosh-up; "none".' },
3020
3076
  soundVolume: { type: "number", description: "0-1." },
3021
3077
  anchorSourceMs: ANCHOR_PROP,
3022
3078
  anchorSourceEndMs: ANCHOR_END_PROP,
@@ -4708,7 +4764,7 @@ const TOOLS = [
4708
4764
  {
4709
4765
  name: "project_set_wallpaper",
4710
4766
  description:
4711
- "Set the background: a wallpaper value, or source=desktop (a kept still of the user's desktop wallpaper) / source=edges (Match video edges: a soft gradient from the video's border colours, slowly following the content; same in preview and export).",
4767
+ "Set the background: a wallpaper value, or source=desktop (a kept still of the user's desktop wallpaper) / source=edges (Match video edges: a soft gradient from the video's border colours, slowly following the content; same in preview and export). Animated: 'mesh:<id>' = a living mesh gradient (blobs drift in a seamless 24 s loop); motion = drift speed.",
4712
4768
  inputSchema: {
4713
4769
  type: "object",
4714
4770
  properties: {
@@ -4717,9 +4773,13 @@ const TOOLS = [
4717
4773
  wallpaper: {
4718
4774
  type: "string",
4719
4775
  description:
4720
- "Bundled id (e.g. gradient-blue), color, CSS gradient or 'none'. Omit with source.",
4776
+ "Bundled id (e.g. gradient-blue), color, CSS gradient, 'none', or an animated mesh:aurora | mesh:sunset | mesh:ocean | mesh:candy | mesh:forest | mesh:midnight | mesh:peach | mesh:graphite. Omit with source.",
4721
4777
  },
4722
4778
  source: { type: "string", enum: ["desktop", "edges"] },
4779
+ motion: {
4780
+ type: "number",
4781
+ description: "Animated (mesh:) drift speed 0-2: 0 still, 1 default. Kept when omitted.",
4782
+ },
4723
4783
  expectedRevision: { type: "number" },
4724
4784
  },
4725
4785
  },
@@ -4779,7 +4839,7 @@ const TOOLS = [
4779
4839
  {
4780
4840
  name: "project_set_style",
4781
4841
  description:
4782
- "Framing style, main-video frame (shape, border ring; drawn when the video is a card) and drawn-cursor behaviour. Only passed fields change. Camera tile of screen+camera: project_set_webcam_style.",
4842
+ "Framing style, main-video frame (shape, border ring; drawn when the video is a card), drawn-cursor behaviour, and the screen's device frame + 3D tilt (screen recordings / images only). Only passed fields change. Camera tile of screen+camera: project_set_webcam_style.",
4783
4843
  inputSchema: {
4784
4844
  type: "object",
4785
4845
  properties: {
@@ -4803,6 +4863,27 @@ const TOOLS = [
4803
4863
  borderWidth: { type: "number", description: "px 0-40." },
4804
4864
  borderColor: { type: "string" },
4805
4865
  resetFrame: { type: "boolean", description: "Clear shape + border first." },
4866
+ deviceFrame: {
4867
+ type: "string",
4868
+ enum: ["browser", "window", "laptop", "phone", "tablet", "none"],
4869
+ description: "Frame around the screen; the video shrinks to fit inside it.",
4870
+ },
4871
+ deviceFrameTheme: { type: "string", enum: ["light", "dark"] },
4872
+ deviceFrameUrl: {
4873
+ type: "string",
4874
+ description: "Browser address / window title; '' clears.",
4875
+ },
4876
+ tilt: {
4877
+ type: "string",
4878
+ enum: ["flat", "left", "right", "above"],
4879
+ description: "3D angle (left / right: that edge goes back; above: seen from above).",
4880
+ },
4881
+ tiltFollowCursor: {
4882
+ type: "boolean",
4883
+ description: "Lean toward the cursor (needs cursor data).",
4884
+ },
4885
+ tiltClickBounce: { type: "boolean", description: "Rock slightly on each click." },
4886
+ tiltStrength: { type: "number", description: "0-100, default 100." },
4806
4887
  expectedRevision: { type: "number" },
4807
4888
  },
4808
4889
  },
@@ -4953,7 +5034,7 @@ const TOOLS = [
4953
5034
  {
4954
5035
  name: "project_align_cursor",
4955
5036
  description:
4956
- "Fix a screen recording whose drawn cursor / cursor auto-zooms land in the wrong place (old recordings saved the pointer with the wrong mapping) without re-recording. Pins: at atMs the REAL pointer (or what was clicked) is at x,y, fractions 0-1 of the recorded frame. 1 pin = offset; 2+ pins far apart also fix scale per axis. Stored on the clip (non-destructive), used by preview, export and auto-zoom; cursor auto-zooms follow unless moveZooms false. reset removes it. Check with project_render_frame.",
5037
+ "Fix a screen recording whose drawn cursor / cursor auto-zooms land in the wrong place (old recordings saved the pointer with the wrong mapping) without re-recording. Pins: at atMs the REAL pointer (or what was clicked) is at x,y, fractions 0-1 of the recorded frame. 1 pin = offset; 2+ pins far apart also fix scale per axis; the LAST pin is matched exactly (earlier ones steer the scale). Stored on the clip (non-destructive), used by preview, export and auto-zoom; cursor auto-zooms follow unless moveZooms false. reset removes it. Check with project_render_frame.",
4957
5038
  inputSchema: {
4958
5039
  type: "object",
4959
5040
  properties: {
@@ -4966,7 +5047,7 @@ const TOOLS = [
4966
5047
  points: {
4967
5048
  type: "array",
4968
5049
  description:
4969
- "[{atMs (edited timeline ms) or sourceMs (clip's own ms), x 0-1, y 0-1}] where the real pointer is.",
5050
+ "[{atMs (edited timeline ms) or sourceMs (clip's own ms), x 0-1, y 0-1}] where the real pointer is. Last pin is exact.",
4970
5051
  items: {
4971
5052
  type: "object",
4972
5053
  properties: {
@@ -5638,7 +5719,7 @@ const TOOLS = [
5638
5719
  {
5639
5720
  name: "audio_enhance",
5640
5721
  description:
5641
- "Enhance voice: THE audio cleanup step (one call). Runs the studio chain on speech, per source (a podcast's host and each guest separately): noise removal, coughs/bumps out of pauses, mouth-click repair, broadcast tone, boom control, level riding (lifts very quiet speech too), de-esser, quieter pauses, -16 LUFS / -1 dBTP. strength: natural (default), light (gentle tidy), strong (assertive broadcast). keepBackground true for vlogs / ambience where the room is part of the video (no noise removal, event remover or pause expander; level, EQ and loudness still run). Enhance voice already removes room echo with the Studio AI model; only use echo when the result reports the model did not run (fallback) (denoiser deepfilternet3, or keepBackground); when the model runs, echo is skipped and the result says echoSkipped: ai-model-removes-echo (relay echoSkippedReason, do not retry). restore true = Studio mic sound (the app's name) for thin, muffled or call-quality mics only (never on a good mic): TF-Restormer on Apple silicon, AP-BWE on Windows and Intel Macs (CPU, faster than real time); restoreEngine in the result says which ran. reference = a clip id or file whose voice tone to match. off true restores the original audio. The noise-removal engine is automatic. Every run processes the original media, so running again replaces the result (never stacks). ASYNC: { jobId }, job_wait; the result reports before/after loudness (LUFS, LRA, true peak, speech over pauses, tone deviation) and the engine per source: tell the user.",
5722
+ "Enhance voice: THE audio cleanup step (one call). Runs the studio chain on speech, per source (a podcast's host and each guest separately): noise removal, coughs/bumps out of pauses, mouth-click repair, broadcast tone, boom control, level riding (lifts very quiet speech too), de-esser, quieter pauses, -16 LUFS / -1 dBTP. strength: light (default, the best-sounding setup with Studio mic sound), natural (fuller processing), strong (assertive broadcast; only when the user asks, never by your own choice). keepBackground true for vlogs / ambience / gameplay where the room is part of the video (no noise removal, event remover or pause expander; level, EQ and loudness still run). A stereo source keeps its stereo (result channels: 2; else channels: 1 with monoReason one-channel / dual-mono / silent / studio-mic), and Studio mic sound stays off unless restore is true (it rebuilds the voice without the room). Enhance voice already removes room echo with the Studio AI model; only use echo when the result reports the model did not run (fallback) (denoiser deepfilternet3, or keepBackground); when the model runs, echo is skipped and the result says echoSkipped: ai-model-removes-echo (relay echoSkippedReason, do not retry). restore = Studio mic sound (the app's name): leave it unset and it is ON on Apple silicon with macOS 14+ (TF-Restormer; Light + Studio mic sound is the default there) and off on Windows and Intel Macs (AP-BWE; pass true there only for thin, muffled or call-quality mics); false only when the user asks. restored / restoreEngine in the result say what ran. reference = a clip id or file whose voice tone to match. off true restores the original audio. The noise-removal engine is automatic. Every run processes the original media, so running again replaces the result (never stacks). ASYNC: { jobId }, job_wait; the result reports before/after loudness (LUFS, LRA, true peak, speech over pauses, tone deviation) and the engine per source: tell the user.",
5642
5723
  inputSchema: {
5643
5724
  type: "object",
5644
5725
  properties: {
@@ -5652,7 +5733,7 @@ const TOOLS = [
5652
5733
  strength: {
5653
5734
  type: "string",
5654
5735
  enum: ["light", "natural", "strong"],
5655
- default: "natural",
5736
+ default: "light",
5656
5737
  },
5657
5738
  keepBackground: {
5658
5739
  type: "boolean",
@@ -5667,9 +5748,8 @@ const TOOLS = [
5667
5748
  },
5668
5749
  restore: {
5669
5750
  type: "boolean",
5670
- default: false,
5671
5751
  description:
5672
- "Studio mic sound for a thin / low-bandwidth mic (TF-Restormer on Apple silicon, AP-BWE elsewhere).",
5752
+ "Studio mic sound. Leave unset: ON on Apple silicon with macOS 14+ (TF-Restormer), off on Windows and Intel Macs (AP-BWE). false only when the user asks.",
5673
5753
  },
5674
5754
  reference: { type: "string", description: "Clip id or audio/video file to sound like." },
5675
5755
  off: { type: "boolean", description: "Remove the enhancement (original audio)." },
@@ -5748,7 +5828,7 @@ const TOOLS = [
5748
5828
  {
5749
5829
  name: "caption_set_template",
5750
5830
  description:
5751
- "Pick a caption template (default glowStack); clears style overrides. Emphasis templates (hormoziEmphasis, tiltedBox, serifItalic, condensedCaps, scriptKeyword) style the IMPORTANT word of each phrase, not the spoken one; emphasis is detected on apply when missing (result.emphasis).",
5831
+ "Pick a caption template (new projects start with hormoziEmphasis when vertical, classic otherwise; use those when the user names no style, never glowStack unless asked); clears style overrides. Emphasis templates (hormoziEmphasis, tiltedBox, serifItalic, condensedCaps, scriptKeyword) style the IMPORTANT word of each phrase, not the spoken one; emphasis is detected on apply when missing (result.emphasis).",
5752
5832
  inputSchema: {
5753
5833
  type: "object",
5754
5834
  properties: {
@@ -6081,10 +6161,55 @@ const TOOLS = [
6081
6161
  description:
6082
6162
  "Hide desktop icons for a whole-display recording (restored on stop/crash/quit). Default: the user's setting. Result tip = ask the user to turn on Do Not Disturb.",
6083
6163
  },
6164
+ windowSize: {
6165
+ type: "string",
6166
+ enum: ["16:9", "16:10", "4:3", "1:1", "9:16", "21:9", "720p", "1080p", "1440p", "4K"],
6167
+ description:
6168
+ "Window sources only: resize the window first. Ratio = same area, new shape; resolution = exact recorded pixels (fails if the display is too small). macOS needs Accessibility access.",
6169
+ },
6170
+ centerWindow: {
6171
+ type: "boolean",
6172
+ description:
6173
+ "Window sources only: move the window to the middle of its display for the recording, back on stop/failure/crash/quit. Default: the user's setting.",
6174
+ },
6084
6175
  },
6085
6176
  },
6086
6177
  command: "recording.start",
6087
6178
  },
6179
+ {
6180
+ name: "recording_resize_window",
6181
+ description:
6182
+ "Resize a window before recording it (the picker's size chips): a ratio keeps its area in the new shape, a resolution makes the recording exactly that size (1080p on a 2x display = a 960 x 540 point window). macOS needs Accessibility access (the error says how). Returns { frame, pixels }.",
6183
+ inputSchema: {
6184
+ type: "object",
6185
+ properties: {
6186
+ source: { type: "string", description: "'window:N:0' from recording_list_sources." },
6187
+ size: {
6188
+ type: "string",
6189
+ enum: ["16:9", "16:10", "4:3", "1:1", "9:16", "21:9", "720p", "1080p", "1440p", "4K"],
6190
+ },
6191
+ },
6192
+ required: ["source", "size"],
6193
+ },
6194
+ command: "recording.resize-window",
6195
+ },
6196
+ {
6197
+ name: "recording_get_window_options",
6198
+ description: "Window-recording options: { centerWindow, supported }.",
6199
+ inputSchema: { type: "object", properties: {} },
6200
+ command: "recording.get-window-options",
6201
+ },
6202
+ {
6203
+ name: "recording_set_center_window",
6204
+ description:
6205
+ "Turn Center on screen on/off for window recordings (window moves to the middle of its display during the recording, back after). Off by default. Returns { centerWindow }.",
6206
+ inputSchema: {
6207
+ type: "object",
6208
+ properties: { enabled: { type: "boolean" } },
6209
+ required: ["enabled"],
6210
+ },
6211
+ command: "recording.set-center-window",
6212
+ },
6088
6213
  {
6089
6214
  name: "recording_get_clean_desktop",
6090
6215
  description: "Clean-desktop recording setting. Returns { cleanDesktop, supported }.",
@@ -6217,7 +6342,7 @@ const TOOLS = [
6217
6342
  {
6218
6343
  name: "project_compose_soundtrack",
6219
6344
  description:
6220
- "Score this edit: music + sound design composed FROM THE PROJECT'S OWN TIMELINE (zooms, graphics, pops, cuts, speech, stressed words) and placed under the voice with ducking; takes over zoom/overlay default sounds; replaces the previous score on re-run. Call LAST, after cuts/zooms/graphics. dryRun returns the score for media_compose_soundtrack. Returns { audioPath, scorePath, events, heroMoments, acts, soundsTakenOver, replaced }.",
6345
+ "Score this edit: music composed FROM THE PROJECT'S OWN TIMELINE (zooms, graphics, pops, cuts, speech, stressed words) placed under the voice with ducking, plus sound effects as recorded library cues (group score-sfx, sparse under a voice); takes over zoom/overlay/graphic sounds; replaces the previous score and its cues on re-run. Call LAST, after cuts/zooms/graphics, and again after changing them (project_read composedScore.stale / doubledSounds). dryRun returns the score and sfxCues. Returns { audioPath, scorePath, events, heroMoments, acts, soundsTakenOver, sfxCues, paperSfx, replaced }.",
6221
6346
  inputSchema: {
6222
6347
  type: "object",
6223
6348
  properties: {
@@ -6229,7 +6354,8 @@ const TOOLS = [
6229
6354
  sfx: { type: "boolean", description: "Default true." },
6230
6355
  useSpeech: {
6231
6356
  type: "boolean",
6232
- description: "Hero hits on the most stressed words. Default true.",
6357
+ description:
6358
+ "Hits on the most stressed words even with nothing on screen. Default true for promo only.",
6233
6359
  },
6234
6360
  cues: {
6235
6361
  type: "array",
@@ -6639,16 +6765,409 @@ const TOOLS = [
6639
6765
  {
6640
6766
  name: "memory_save",
6641
6767
  description:
6642
- "Save a durable workspace fact or preference for all future chats (not one-off requests).",
6768
+ "Save a durable fact or preference the user STATED, for all future chats (not one-off requests). kind profile = who they are. Over budget it's refused with the current entries: merge (memory_replace) or forget first. Noticed but not stated: memory_suggest.",
6643
6769
  inputSchema: {
6644
6770
  type: "object",
6645
6771
  properties: {
6646
6772
  note: { type: "string", description: "One concise fact." },
6773
+ kind: {
6774
+ type: "string",
6775
+ enum: ["notes", "profile"],
6776
+ description: "notes (default): preferences. profile: who the creator is.",
6777
+ },
6647
6778
  },
6648
6779
  required: ["note"],
6649
6780
  },
6650
6781
  command: "memory.save",
6651
6782
  },
6783
+ {
6784
+ name: "memory_replace",
6785
+ description: "Rewrite one memory entry by id: correct it, or merge two to stay under budget.",
6786
+ inputSchema: {
6787
+ type: "object",
6788
+ properties: {
6789
+ entryId: { type: "string", description: "Entry id from memory_list." },
6790
+ note: { type: "string", description: "The new text." },
6791
+ kind: {
6792
+ type: "string",
6793
+ enum: ["notes", "profile"],
6794
+ description: "notes (default): preferences. profile: who the creator is.",
6795
+ },
6796
+ },
6797
+ required: ["entryId", "note"],
6798
+ },
6799
+ command: "memory.replace",
6800
+ },
6801
+ {
6802
+ name: "memory_suggest",
6803
+ description:
6804
+ "PandaStudio's own chat only: a 'Remember this?' card for a preference you noticed but the user didn't state. Their Save writes it; don't save it yourself.",
6805
+ inputSchema: {
6806
+ type: "object",
6807
+ properties: {
6808
+ note: { type: "string", description: "The preference, worded as it would be saved." },
6809
+ reason: { type: "string", description: "One line on why you think so." },
6810
+ kind: {
6811
+ type: "string",
6812
+ enum: ["notes", "profile"],
6813
+ description: "notes (default): preferences. profile: who the creator is.",
6814
+ },
6815
+ },
6816
+ required: ["note"],
6817
+ },
6818
+ command: "memory.suggest",
6819
+ },
6820
+ {
6821
+ name: "chat_ask",
6822
+ description:
6823
+ "PandaStudio's own chat only: ask the user a multiple-choice question as a card with 2-5 buttons. Doesn't wait; END YOUR TURN after calling it and the choice arrives as the user's next message. For real decisions (light or heavy edit, which opening), not things you can decide yourself. Other agents get not_supported: ask in your own chat.",
6824
+ inputSchema: {
6825
+ type: "object",
6826
+ properties: {
6827
+ question: { type: "string", description: "One short sentence." },
6828
+ options: {
6829
+ type: "array",
6830
+ description: "2-5 choices: strings, or { label, detail } with detail one short line.",
6831
+ items: {
6832
+ anyOf: [
6833
+ { type: "string" },
6834
+ {
6835
+ type: "object",
6836
+ properties: { label: { type: "string" }, detail: { type: "string" } },
6837
+ required: ["label"],
6838
+ },
6839
+ ],
6840
+ },
6841
+ },
6842
+ recommended: { type: "string", description: "Label of the choice you'd pick." },
6843
+ allowOther: { type: "boolean", description: "Also allow a typed answer." },
6844
+ },
6845
+ required: ["question", "options"],
6846
+ },
6847
+ command: "chat.ask",
6848
+ },
6849
+ {
6850
+ name: "motion_propose",
6851
+ description:
6852
+ "PandaStudio's own chat: show a rendered graphic playing in the chat with Add to timeline / Try another before placing it. Render first (motion_generate or motion_render_html, job_wait), then pass the jobId and where it goes. For graphic ideas and judgment calls; a graphic the user asked for goes straight in with project_add_motion_graphic.",
6853
+ inputSchema: {
6854
+ type: "object",
6855
+ properties: {
6856
+ id: { type: "string", description: "Project id" },
6857
+ path: { type: "string" },
6858
+ fromJob: { type: "string", description: "Finished render job (preferred)." },
6859
+ file: { type: "string", description: "Rendered MP4/WebM when there's no job." },
6860
+ atMs: { type: "number", description: "Edited timeline start." },
6861
+ durationMs: { type: "number", description: "Default: the render's length." },
6862
+ title: { type: "string", description: "What it shows, a few words." },
6863
+ reason: { type: "string", description: "Why at this moment." },
6864
+ },
6865
+ required: ["atMs", "title"],
6866
+ },
6867
+ command: "motion.propose",
6868
+ },
6869
+ {
6870
+ name: "post_plan",
6871
+ description:
6872
+ "PandaStudio's own chat: suggest where and when to post finished exports, a caption each, as an editable schedule card. Nothing posts until the user approves; then each row is queued via export_publish_social and you get the job ids. Export ids from export_list, accounts from social_channels.",
6873
+ inputSchema: {
6874
+ type: "object",
6875
+ properties: {
6876
+ summary: { type: "string", description: "One line on the plan." },
6877
+ rows: {
6878
+ type: "array",
6879
+ items: {
6880
+ type: "object",
6881
+ properties: {
6882
+ exportId: { type: "string" },
6883
+ channelIds: { type: "array", items: { type: "string" } },
6884
+ networks: {
6885
+ type: "array",
6886
+ items: {
6887
+ type: "string",
6888
+ enum: ["instagram", "tiktok", "facebook", "linkedin", "x"],
6889
+ },
6890
+ },
6891
+ caption: { type: "string" },
6892
+ when: {
6893
+ type: "string",
6894
+ description: "now, or ISO with offset (e.g. 2026-11-03T18:30:00+00:00).",
6895
+ },
6896
+ },
6897
+ required: ["exportId", "caption"],
6898
+ },
6899
+ },
6900
+ },
6901
+ required: ["rows"],
6902
+ },
6903
+ command: "post.plan",
6904
+ },
6905
+ {
6906
+ name: "plan_propose",
6907
+ description:
6908
+ "Write an editing plan for the user to approve BEFORE editing (summary, up to 3 questions, up to 20 steps). Shown as a plan card with Start editing; nothing changes yet. End your turn after proposing; when they press Start you get the planId and answers, then do the steps with plan_update_step after each. Mark needsApproval on steps that spend money, publish or reach outside the app.",
6909
+ inputSchema: {
6910
+ type: "object",
6911
+ properties: {
6912
+ id: { type: "string", description: "Project id" },
6913
+ path: { type: "string", description: "Absolute .pandastudio path (alternative to id)" },
6914
+ summary: { type: "string", description: "One line: what the footage is and the angle." },
6915
+ steps: {
6916
+ type: "array",
6917
+ description: "Steps in order.",
6918
+ items: {
6919
+ type: "object",
6920
+ properties: {
6921
+ title: { type: "string" },
6922
+ stage: { type: "string", enum: ["prep", "edit", "clip", "post"] },
6923
+ detail: { type: "string" },
6924
+ needsApproval: { type: "boolean" },
6925
+ },
6926
+ required: ["title"],
6927
+ },
6928
+ },
6929
+ questions: {
6930
+ type: "array",
6931
+ description: "Decisions only the user can make (max 3).",
6932
+ items: {
6933
+ type: "object",
6934
+ properties: {
6935
+ text: { type: "string" },
6936
+ options: { type: "array", items: { type: "string" } },
6937
+ recommended: { type: "string" },
6938
+ },
6939
+ required: ["text", "options"],
6940
+ },
6941
+ },
6942
+ recipeId: { type: "string", description: "Recipe the plan follows (recipe_pick)." },
6943
+ source: { type: "string", enum: ["chat", "recording-finished", "routine"] },
6944
+ },
6945
+ required: ["summary", "steps"],
6946
+ },
6947
+ command: "plan.propose",
6948
+ },
6949
+ {
6950
+ name: "plan_get",
6951
+ description:
6952
+ "The project's open plan (proposed or running) with step statuses and the next step; plan: null when none.",
6953
+ inputSchema: {
6954
+ type: "object",
6955
+ properties: {
6956
+ id: { type: "string" },
6957
+ path: { type: "string" },
6958
+ includeHistory: {
6959
+ type: "boolean",
6960
+ description: "Also return finished and dismissed plans.",
6961
+ },
6962
+ },
6963
+ },
6964
+ command: "plan.get",
6965
+ },
6966
+ {
6967
+ name: "plan_answer",
6968
+ description:
6969
+ "Record the user's answer to a plan question (option id or label). Only before the plan starts.",
6970
+ inputSchema: {
6971
+ type: "object",
6972
+ properties: {
6973
+ id: { type: "string" },
6974
+ path: { type: "string" },
6975
+ planId: { type: "string" },
6976
+ questionId: { type: "string" },
6977
+ optionId: { type: "string" },
6978
+ },
6979
+ required: ["questionId", "optionId"],
6980
+ },
6981
+ command: "plan.answer",
6982
+ },
6983
+ {
6984
+ name: "plan_start",
6985
+ description:
6986
+ "Start a proposed plan (unanswered questions take the recommended choice). The card's Start editing does this; call it only when the user told you to go ahead.",
6987
+ inputSchema: {
6988
+ type: "object",
6989
+ properties: { id: { type: "string" }, path: { type: "string" }, planId: { type: "string" } },
6990
+ },
6991
+ command: "plan.start",
6992
+ },
6993
+ {
6994
+ name: "plan_update_step",
6995
+ description:
6996
+ "Mark a step of the running plan: running, then done (result with the real counts), skipped or failed (say why).",
6997
+ inputSchema: {
6998
+ type: "object",
6999
+ properties: {
7000
+ id: { type: "string" },
7001
+ path: { type: "string" },
7002
+ planId: { type: "string" },
7003
+ stepId: { type: "string" },
7004
+ status: { type: "string", enum: ["running", "done", "skipped", "failed"] },
7005
+ result: { type: "string" },
7006
+ },
7007
+ required: ["stepId", "status"],
7008
+ },
7009
+ command: "plan.update-step",
7010
+ },
7011
+ {
7012
+ name: "plan_finish",
7013
+ description: "Close a plan: done (unreached steps become skipped) or dismissed.",
7014
+ inputSchema: {
7015
+ type: "object",
7016
+ properties: {
7017
+ id: { type: "string" },
7018
+ path: { type: "string" },
7019
+ planId: { type: "string" },
7020
+ status: { type: "string", enum: ["done", "dismissed"] },
7021
+ },
7022
+ required: ["status"],
7023
+ },
7024
+ command: "plan.finish",
7025
+ },
7026
+ {
7027
+ name: "routine_templates",
7028
+ description: "Ready-made routines with the exact settings for routine_propose.",
7029
+ inputSchema: { type: "object", properties: {} },
7030
+ command: "routine.templates",
7031
+ },
7032
+ {
7033
+ name: "routine_list",
7034
+ description:
7035
+ "This workspace's routines (proposed / on / paused), triggers in plain words, next run and recent runs with reports.",
7036
+ inputSchema: { type: "object", properties: {} },
7037
+ command: "routine.list",
7038
+ },
7039
+ {
7040
+ name: "routine_propose",
7041
+ description:
7042
+ "Propose a routine: recurring or event-triggered work done on its own. Created OFF; the user turns it on from a routine card. Say what it does, how often, and that it runs on their AI account.",
7043
+ inputSchema: {
7044
+ type: "object",
7045
+ properties: {
7046
+ name: { type: "string", description: "A few words, e.g. 'Monday competitor scan'." },
7047
+ prompt: {
7048
+ type: "string",
7049
+ description: "What the agent does each run (unattended: no questions, read-only shell).",
7050
+ },
7051
+ schedule: {
7052
+ type: "string",
7053
+ description:
7054
+ "'every 2h', 'daily 09:00', 'weekdays 09:00', 'mon,thu 18:30', 'monthly 1 09:00' or an ISO time. Schedule OR event; min every 15 min.",
7055
+ },
7056
+ timezone: { type: "string", description: "IANA zone; default this Mac's." },
7057
+ event: {
7058
+ type: "string",
7059
+ enum: ["recording.finished", "import.finished", "podcast.ended", "export.finished"],
7060
+ description: "Run when this happens instead of on a schedule.",
7061
+ },
7062
+ precheck: {
7063
+ type: "object",
7064
+ description:
7065
+ "Free check before the AI turn; nothing found = run skipped. {kind:'ideas-breakouts',minMultiplier} | {kind:'new-exports'} | {kind:'connector-connected',connector}.",
7066
+ },
7067
+ autonomy: {
7068
+ type: "string",
7069
+ enum: ["suggest", "act"],
7070
+ description: "suggest (default): no edits. act: edits that need no approval.",
7071
+ },
7072
+ notify: { type: "string", enum: ["desktop", "none"] },
7073
+ template: { type: "string", description: "Start from a routine_templates id." },
7074
+ },
7075
+ required: ["name", "prompt"],
7076
+ },
7077
+ command: "routine.propose",
7078
+ },
7079
+ {
7080
+ name: "routine_update",
7081
+ description:
7082
+ "Change a routine. name/notify apply now; prompt, schedule/event, precheck or autonomy become a pending change the user approves on the card.",
7083
+ inputSchema: {
7084
+ type: "object",
7085
+ properties: {
7086
+ routineId: { type: "string" },
7087
+ name: { type: "string", description: "A few words, e.g. 'Monday competitor scan'." },
7088
+ prompt: {
7089
+ type: "string",
7090
+ description: "What the agent does each run (unattended: no questions, read-only shell).",
7091
+ },
7092
+ schedule: {
7093
+ type: "string",
7094
+ description:
7095
+ "'every 2h', 'daily 09:00', 'weekdays 09:00', 'mon,thu 18:30', 'monthly 1 09:00' or an ISO time. Schedule OR event; min every 15 min.",
7096
+ },
7097
+ timezone: { type: "string", description: "IANA zone; default this Mac's." },
7098
+ event: {
7099
+ type: "string",
7100
+ enum: ["recording.finished", "import.finished", "podcast.ended", "export.finished"],
7101
+ description: "Run when this happens instead of on a schedule.",
7102
+ },
7103
+ precheck: {
7104
+ type: "object",
7105
+ description:
7106
+ "Free check before the AI turn; nothing found = run skipped. {kind:'ideas-breakouts',minMultiplier} | {kind:'new-exports'} | {kind:'connector-connected',connector}.",
7107
+ },
7108
+ autonomy: {
7109
+ type: "string",
7110
+ enum: ["suggest", "act"],
7111
+ description: "suggest (default): no edits. act: edits that need no approval.",
7112
+ },
7113
+ notify: { type: "string", enum: ["desktop", "none"] },
7114
+ },
7115
+ required: ["routineId"],
7116
+ },
7117
+ command: "routine.update",
7118
+ },
7119
+ {
7120
+ name: "routine_pause",
7121
+ description: "Pause a routine.",
7122
+ inputSchema: {
7123
+ type: "object",
7124
+ properties: { routineId: { type: "string" } },
7125
+ required: ["routineId"],
7126
+ },
7127
+ command: "routine.pause",
7128
+ },
7129
+ {
7130
+ name: "routine_resume",
7131
+ description: "Resume a paused routine (not a proposal).",
7132
+ inputSchema: {
7133
+ type: "object",
7134
+ properties: { routineId: { type: "string" } },
7135
+ required: ["routineId"],
7136
+ },
7137
+ command: "routine.resume",
7138
+ },
7139
+ {
7140
+ name: "routine_run",
7141
+ description:
7142
+ "Run a routine now in the background; the report lands in the Routines thread and routine_runs.",
7143
+ inputSchema: {
7144
+ type: "object",
7145
+ properties: { routineId: { type: "string" } },
7146
+ required: ["routineId"],
7147
+ },
7148
+ command: "routine.run",
7149
+ },
7150
+ {
7151
+ name: "routine_runs",
7152
+ description:
7153
+ "A routine's runs, newest first: done, silent, skipped, failed, missed, interrupted; with reports.",
7154
+ inputSchema: {
7155
+ type: "object",
7156
+ properties: { routineId: { type: "string" }, limit: { type: "number" } },
7157
+ required: ["routineId"],
7158
+ },
7159
+ command: "routine.runs",
7160
+ },
7161
+ {
7162
+ name: "routine_delete",
7163
+ description: "Delete a routine and its history (asks the user on a confirm card).",
7164
+ inputSchema: {
7165
+ type: "object",
7166
+ properties: { routineId: { type: "string" } },
7167
+ required: ["routineId"],
7168
+ },
7169
+ command: "routine.delete",
7170
+ },
6652
7171
  {
6653
7172
  name: "recipe_list",
6654
7173
  description: "List edit recipes (proven styles): the user's saved ones first, then starters.",
@@ -6682,13 +7201,17 @@ const TOOLS = [
6682
7201
  {
6683
7202
  name: "recipe_pick",
6684
7203
  description:
6685
- "No style named ('edit my video', 'make this a Short')? The recipes that fit this project's format and footage, each with pickWhen, plus defaultId. Choose by what's said, then recipe_apply_style + recipe_render.",
7204
+ "No style named ('edit my video', 'make this a Short')? The recipes that fit this project's format and footage, each with pickWhen, plus defaultId. Choose by what's said (defaultId when nothing clearly fits, never a plain clean-up), then recipe_apply_style + recipe_render. A Short cut from a landscape long-form video (a project_fork_from_shot fork, a landscape source, or format short on a 16:9 project) comes back fromLongForm with its own default. On a 16:9 project the user wants as a Short, pass format short.",
6686
7205
  inputSchema: {
6687
7206
  type: "object",
6688
7207
  properties: {
6689
7208
  id: { type: "string", description: "Project id (default: the open project)." },
6690
7209
  path: { type: "string" },
6691
- format: { type: "string", enum: ["short", "long"] },
7210
+ format: {
7211
+ type: "string",
7212
+ enum: ["short", "long"],
7213
+ description: "Default from the project's aspect ratio (9:16 = short).",
7214
+ },
6692
7215
  footage: {
6693
7216
  type: "string",
6694
7217
  enum: ["camera", "screen", "podcast", "audio", "none"],
@@ -6791,6 +7314,11 @@ const TOOLS = [
6791
7314
  type: "object",
6792
7315
  properties: {
6793
7316
  query: { type: "string", description: "Entry id, or text removing every match." },
7317
+ kind: {
7318
+ type: "string",
7319
+ enum: ["notes", "profile"],
7320
+ description: "notes (default): preferences. profile: who the creator is.",
7321
+ },
6794
7322
  },
6795
7323
  required: ["query"],
6796
7324
  },
@@ -6798,8 +7326,17 @@ const TOOLS = [
6798
7326
  },
6799
7327
  {
6800
7328
  name: "memory_list",
6801
- description: "List durable memory entries (ids for memory_forget).",
6802
- inputSchema: { type: "object", properties: {} },
7329
+ description: "Both memory lists (notes, profile) with ids and budget use; kind for one list.",
7330
+ inputSchema: {
7331
+ type: "object",
7332
+ properties: {
7333
+ kind: {
7334
+ type: "string",
7335
+ enum: ["notes", "profile"],
7336
+ description: "notes (default): preferences. profile: who the creator is.",
7337
+ },
7338
+ },
7339
+ },
6803
7340
  command: "memory.list",
6804
7341
  },
6805
7342
  {
@@ -7423,7 +7960,7 @@ const TOOLS = [
7423
7960
  },
7424
7961
  captionTemplate: {
7425
7962
  type: "string",
7426
- description: "Caption template id (default glowStack), or none.",
7963
+ description: "Caption template id (default hormoziEmphasis), or none.",
7427
7964
  },
7428
7965
  },
7429
7966
  required: ["file"],
@@ -7496,6 +8033,265 @@ const TOOLS = [
7496
8033
  },
7497
8034
  command: "clips.delete",
7498
8035
  },
8036
+ {
8037
+ name: "clips_remove_clip",
8038
+ description:
8039
+ "Remove ONE clip from a clip set (the Clips screen's Remove on a clip card). Its stills go and its moment is remembered so clips.retry won't make it again; its project moves to the Trash (recoverable) unless keepProject is true. Exported files stay in My Exports. A clip still being made is dropped by the running job. Returns { clipSetId, removed, projectDeleted, clipsLeft }.",
8040
+ inputSchema: {
8041
+ type: "object",
8042
+ properties: {
8043
+ id: { type: "string", description: "Clip set id." },
8044
+ clipId: { type: "string", description: "The clip's id (from clips_get)." },
8045
+ keepProject: {
8046
+ type: "boolean",
8047
+ description: "Keep the clip's project in Projects instead of the Trash. Default false.",
8048
+ },
8049
+ },
8050
+ required: ["id", "clipId"],
8051
+ },
8052
+ command: "clips.remove-clip",
8053
+ },
8054
+ {
8055
+ name: "ideas_research",
8056
+ description:
8057
+ "Find outliers (videos beating their own channel's median views) across a niche: searches YouTube for the topic (comma-separate several angles) and/or expands from a seed channel the target audience watches, reads every channel found and ranks their recent uploads by multiplier. For what's trending, pass period month and maxAgeDays 30. Async: returns { researchId, jobId }; job.wait the jobId (~20-40 s), then ideas.get.",
8058
+ inputSchema: {
8059
+ type: "object",
8060
+ properties: {
8061
+ query: {
8062
+ type: "string",
8063
+ description: 'Topic(s), comma-separated: "ai video editing, descript alternatives".',
8064
+ },
8065
+ seedChannel: {
8066
+ type: "string",
8067
+ description:
8068
+ "A channel the target audience watches: @handle, channel URL or UC id. Give this, query, or both.",
8069
+ },
8070
+ format: { type: "string", description: "videos (long-form, default) | shorts." },
8071
+ minMultiplier: {
8072
+ type: "number",
8073
+ description:
8074
+ "Keep videos at or above this multiple of their channel's median (default 3).",
8075
+ },
8076
+ maxAgeDays: {
8077
+ type: "number",
8078
+ description: "Only outliers published in the last N days (default 365, 0 = any age).",
8079
+ },
8080
+ maxChannels: {
8081
+ type: "number",
8082
+ description: "Channels to read, 3-40 (default 15). More is slower and broader.",
8083
+ },
8084
+ period: {
8085
+ type: "string",
8086
+ description:
8087
+ "Limit the topic search to uploads from the last: any (default) | day | week | month | year.",
8088
+ },
8089
+ },
8090
+ },
8091
+ command: "ideas.research",
8092
+ },
8093
+ {
8094
+ name: "ideas_list",
8095
+ description:
8096
+ "Every outlier research in this workspace, newest first: id, title, options, status, stats and top multiplier. Reuse a recent one instead of researching the same niche again.",
8097
+ inputSchema: {
8098
+ type: "object",
8099
+ properties: {},
8100
+ },
8101
+ command: "ideas.list",
8102
+ },
8103
+ {
8104
+ name: "ideas_get",
8105
+ description:
8106
+ "One research: status, outliers best first (title, url, multiplier, views, channel median, age; onTopic false marks titles that don't mention the topic), the channels read (median views, hit rate, uploads a week) and title patterns (recurring phrases, hook-style shares, title formulas).",
8107
+ inputSchema: {
8108
+ type: "object",
8109
+ properties: {
8110
+ researchId: { type: "string", description: "From ideas.research or ideas.list." },
8111
+ limit: { type: "number", description: "Max outliers returned, 1-200 (default 25)." },
8112
+ offset: { type: "number", description: "Skip this many outliers (to page)." },
8113
+ onTopicOnly: {
8114
+ type: "boolean",
8115
+ description: "Only outliers whose titles mention the topic.",
8116
+ },
8117
+ },
8118
+ required: ["researchId"],
8119
+ },
8120
+ command: "ideas.get",
8121
+ },
8122
+ {
8123
+ name: "ideas_cancel",
8124
+ description: "Stop a running research.",
8125
+ inputSchema: {
8126
+ type: "object",
8127
+ properties: {
8128
+ researchId: { type: "string", description: "The research to stop." },
8129
+ },
8130
+ required: ["researchId"],
8131
+ },
8132
+ command: "ideas.cancel",
8133
+ },
8134
+ {
8135
+ name: "ideas_delete",
8136
+ description: "Forget a research.",
8137
+ inputSchema: {
8138
+ type: "object",
8139
+ properties: {
8140
+ researchId: { type: "string", description: "The research to delete." },
8141
+ },
8142
+ required: ["researchId"],
8143
+ },
8144
+ command: "ideas.delete",
8145
+ },
8146
+ {
8147
+ name: "ideas_channel",
8148
+ description:
8149
+ "One channel's baseline (median views of its recent uploads, hit rate, uploads a week, subscribers) and its uploads that beat it, best first. Use on a competitor, or on the user's own channel to see what already works for them. ~10 s.",
8150
+ inputSchema: {
8151
+ type: "object",
8152
+ properties: {
8153
+ channel: { type: "string", description: "@handle, channel URL or UC id." },
8154
+ format: { type: "string", description: "videos (long-form, default) | shorts." },
8155
+ minMultiplier: { type: "number", description: "Breakout threshold (default 1.5)." },
8156
+ limit: { type: "number", description: "Recent uploads to read, 10-100 (default 40)." },
8157
+ recent: {
8158
+ type: "boolean",
8159
+ description: "Also return every upload read, newest first (default false).",
8160
+ },
8161
+ },
8162
+ required: ["channel"],
8163
+ },
8164
+ command: "ideas.channel",
8165
+ },
8166
+ {
8167
+ name: "ideas_similar",
8168
+ description:
8169
+ "Channels competing for a channel's audience: searches the titles of its best uploads and ranks the channels that keep showing up (matches), each with its baseline (median views, hit rate, uploads a week, subscribers). ~30-60 s.",
8170
+ inputSchema: {
8171
+ type: "object",
8172
+ properties: {
8173
+ channel: { type: "string", description: "@handle, channel URL or UC id." },
8174
+ format: { type: "string", description: "videos (long-form, default) | shorts." },
8175
+ max: { type: "number", description: "Channels to return, 3-25 (default 12)." },
8176
+ },
8177
+ required: ["channel"],
8178
+ },
8179
+ command: "ideas.similar",
8180
+ },
8181
+ {
8182
+ name: "ideas_video",
8183
+ description:
8184
+ "A YouTube video's stats (views, likes, comment count, subscribers), description, tags and chapters. With baseline, also its multiplier against its channel's median: did it break out? With comments > 0, its top comments by likes: what viewers ask for and push back on.",
8185
+ inputSchema: {
8186
+ type: "object",
8187
+ properties: {
8188
+ video: { type: "string", description: "YouTube URL or 11-character id." },
8189
+ baseline: {
8190
+ type: "boolean",
8191
+ description:
8192
+ "Also read its channel's recent uploads and return multiplier and channelMedian. Adds ~5 s.",
8193
+ },
8194
+ comments: {
8195
+ type: "number",
8196
+ description: "Top comments to read, 0-500 (default 0). 100 takes ~10-20 s.",
8197
+ },
8198
+ },
8199
+ required: ["video"],
8200
+ },
8201
+ command: "ideas.video",
8202
+ },
8203
+ {
8204
+ name: "ideas_transcript",
8205
+ description:
8206
+ "A YouTube video's transcript from its captions (uploaded first, else automatic): to study an outlier's hook, structure and pacing.",
8207
+ inputSchema: {
8208
+ type: "object",
8209
+ properties: {
8210
+ video: { type: "string", description: "YouTube URL or 11-character id." },
8211
+ language: { type: "string", description: "Caption language code (default en)." },
8212
+ timestamps: {
8213
+ type: "boolean",
8214
+ description: "Return timed segments instead of plain text (default false).",
8215
+ },
8216
+ },
8217
+ required: ["video"],
8218
+ },
8219
+ command: "ideas.transcript",
8220
+ },
8221
+ {
8222
+ name: "ideas_keywords",
8223
+ description:
8224
+ "What people type into YouTube search around a seed phrase (autocomplete, expanded a-z and with question prefixes). Then ideas.demand on the promising ones.",
8225
+ inputSchema: {
8226
+ type: "object",
8227
+ properties: {
8228
+ seed: { type: "string", description: 'Seed phrase, e.g. "notion template".' },
8229
+ expand: {
8230
+ type: "boolean",
8231
+ description: "Expand with a-z and question prefixes (default true).",
8232
+ },
8233
+ },
8234
+ required: ["seed"],
8235
+ },
8236
+ command: "ideas.keywords",
8237
+ },
8238
+ {
8239
+ name: "ideas_demand",
8240
+ description:
8241
+ "How much a few YouTube search terms pull: for each, the median and top views of its top 20 results, the share under a year old, how many channels, and the 3 most-viewed. Pick the term worth a video after ideas.keywords. ~30 s for 10 terms.",
8242
+ inputSchema: {
8243
+ type: "object",
8244
+ properties: {
8245
+ keywords: {
8246
+ type: "array",
8247
+ items: { type: "string" },
8248
+ description: "Search terms to check (1-15).",
8249
+ },
8250
+ },
8251
+ required: ["keywords"],
8252
+ },
8253
+ command: "ideas.demand",
8254
+ },
8255
+ {
8256
+ name: "ideas_competitors",
8257
+ description:
8258
+ "The channels this workspace tracks, each with its baseline at the last scan and its uploads from the last 90 days at 2x or more (breakouts). Remembered across conversations; ideas.scan refreshes them.",
8259
+ inputSchema: {
8260
+ type: "object",
8261
+ properties: {},
8262
+ },
8263
+ command: "ideas.competitors",
8264
+ },
8265
+ {
8266
+ name: "ideas_track",
8267
+ description:
8268
+ "Start tracking a channel as a competitor (reads it once), or stop with tracked: false. Up to 40 per workspace.",
8269
+ inputSchema: {
8270
+ type: "object",
8271
+ properties: {
8272
+ channel: {
8273
+ type: "string",
8274
+ description: "@handle, channel URL or UC id (to stop: the UC id from ideas.competitors).",
8275
+ },
8276
+ tracked: { type: "boolean", description: "true (default) to track, false to stop." },
8277
+ format: { type: "string", description: "videos (long-form, default) | shorts." },
8278
+ },
8279
+ required: ["channel"],
8280
+ },
8281
+ command: "ideas.track",
8282
+ },
8283
+ {
8284
+ name: "ideas_scan",
8285
+ description:
8286
+ "Re-read every tracked competitor and return the breakouts across all of them (uploads from the last 90 days at 2x or more), best first. ~5-10 s per 5 channels.",
8287
+ inputSchema: {
8288
+ type: "object",
8289
+ properties: {
8290
+ limit: { type: "number", description: "Max breakouts returned, 1-100 (default 20)." },
8291
+ },
8292
+ },
8293
+ command: "ideas.scan",
8294
+ },
7499
8295
  {
7500
8296
  name: "export_generate_shots",
7501
8297
  description:
@@ -7902,14 +8698,15 @@ Read-only tools (project_read, transcript_get / _search / _find_issues, audio_pr
7902
8698
 
7903
8699
  Before an editing pass, read the skill's "Which tool for which moment" section (skill_read section="Which tool for which moment").
7904
8700
 
7905
- "Edit my video" / "make this a Short" with no specifics = this pipeline, in order. Confirm scope once ("full polish, or just part of it?"), then run it all:
8701
+ "Edit my video" / "make this a Short" with no specifics = this pipeline, in order. In PandaStudio's own chat: write it as a plan first (plan_propose: the steps below that apply, the scope question on it), end your turn, and run it when the user presses Start editing, with plan_update_step after each step. Other clients: confirm scope once ("full polish, or just part of it?"), then run it all:
7906
8702
  1. transcript_transcribe clips not yet transcribed (project_read clipStates).
7907
8703
  2. transcript_remove_fillers.
7908
8704
  3. Fix misheard names / brands with transcript_find_replace (transcript_insert_words for dropped words).
7909
8705
  4. transcript_find_issues, then transcript_delete_words on each duplicate take / false start (keep the LAST take; keep severity "low" ones).
7910
8706
  5. transcript_remove_silences.
7911
- 6. audio_enhance (Enhance voice) on clips not yet enhanced: strength natural; keepBackground true for vlogs / ambience; no echo (Enhance voice already removes room echo with the Studio AI model; only use echo when the result reports the model did not run (fallback)).
7912
- 7. The style, after steps 2-5 (they shift the timeline). The user's named style, recipe, creator or a saved memory wins. Otherwise call recipe_pick: it returns the recipes that fit this project's format and footage, each with pickWhen, plus defaultId. Read the transcript, choose the ONE candidate whose pickWhen matches what is said (defaultId when none clearly fits), do its "before" step if any, then recipe_apply_style + recipe_render and follow the prompt: the recipe decides the style (captions, pacing, kinds of moves, music, transitions). Graphics inside it are designed by you: a named template is the reference look; author your own (motion_render_html, same palette and type) when a beat needs something the template can't show. (An app without recipe_pick, 2.0.5 and older: recipe_list with format short or long, and choose by description and footage.) Tell the user which recipe and why in one line and name one or two alternatives. Only when no candidate exists or the user asked for a plain clean-up: captions (caption_toggle, template glowStack), a few motion graphics, emphasis zooms.
8707
+ 5b. A Short from a landscape video (a long-form recording, the project isn't 9:16): cut the Short out now. export_generate_shots { projectId }, then project_fork_from_shot on the best shot (offer the top shots when the user is there) and run steps 6-9 on the fork. Only a clip already under about 90 s goes vertical in place (project_set_aspect_ratio 9:16).
8708
+ 6. audio_enhance (Enhance voice) on clips not yet enhanced with its defaults (Light, plus Studio mic sound on Apple silicon; don't pass strength or restore unless the user asked); keepBackground true for vlogs / ambience; no echo (Enhance voice already removes room echo with the Studio AI model; only use echo when the result reports the model did not run (fallback)).
8709
+ 7. The style, after steps 2-5 (they shift the timeline). The user's named style, recipe, creator or a saved memory wins. Otherwise call recipe_pick: it returns the recipes that fit this project's format and footage, each with pickWhen, plus defaultId. Read the transcript, choose the ONE candidate whose pickWhen matches what is said (defaultId when none clearly fits; a Short cut from a long-form video comes back fromLongForm and its defaultId is made for that), do its "before" step if any, then recipe_apply_style + recipe_render and follow the prompt: the recipe decides the style (captions, pacing, kinds of moves, music, transitions). Graphics inside it are designed by you: a named template is the reference look; author your own (motion_render_html, same palette and type) when a beat needs something the template can't show. (An app without recipe_pick, 2.0.5 and older: recipe_list with format short or long, and choose by description and footage.) Tell the user which recipe and why in one line and name one or two alternatives. Never trade the default for a plain edit because no candidate fits perfectly. Only when no candidate exists or the user asked for a plain clean-up: captions (caption_toggle, then caption_set_template: hormoziEmphasis for vertical, classic for long-form; not glowStack unless asked), a few motion graphics, emphasis zooms.
7913
8710
  8. B-roll, last: pick the moments yourself from what's said (any language), check them with project_check_broll, show the user your moments and ASK: their own images, generated on their image connector (only if one is connected; it uses their credits), or no B-roll. Never generate or invent images without their answer. Place what they give with project_add_broll. Nobody to ask: skip it and say so.
7914
8711
  9. llm_generate_title / description / timestamps, then preview_show and ask before export_start. Never export before the user has seen the preview.
7915
8712
  Report the real count each step returned. No music, intro/outro cards or FX overlays unless the recipe or the user asks for them.
@@ -7923,6 +8720,8 @@ Publishing has daily limits (YouTube uploads and updates per license, social pos
7923
8720
 
7924
8721
  PandaStudio problems (a capability gap like an imported font the export can't draw, a command failing unexpectedly, a render not matching the project): call system_report_issue yourself, then tell the user you reported it. Not for user mistakes, missing keys or taste.
7925
8722
 
8723
+ Views: in chat apps that show MCP views, preview_show, project_render_frame / _render_sheet and project_edit_summary also show the frames and what the edit contains, and clips_get / clips_make show the clips as cards the user can open, export or remove. Summarise in a line instead of describing each frame or clip; what the user does in a view arrives as context, don't redo it.
8724
+
7926
8725
  Review items: the user can pin notes and drawings on the preview and send them in one turn (listed in the editor context and the message, crops attached in item order). Apply each one, then review_resolve with its id. review_list shows the open ones with a crop image path.
7927
8726
 
7928
8727
  Destination decides everything: infer it (Shorts/TikTok/vertical/<=90s: shorts; YouTube/tutorial/>8 min: youtube-long; LinkedIn; Loom/internal), else ask once.
@@ -7937,23 +8736,49 @@ const server = new Server(
7937
8736
  {
7938
8737
  capabilities: {
7939
8738
  tools: {},
8739
+ // Only the ui:// views (MCP Apps), listed for clients that render them.
8740
+ resources: {},
7940
8741
  },
7941
8742
  instructions: INSTRUCTIONS,
7942
8743
  },
7943
8744
  );
7944
8745
 
8746
+ /** Whether the connected client renders MCP Apps views (apps/index.mjs). */
8747
+ const uiEnabled = () => clientSupportsUi(server.getClientCapabilities(), { inApp: IN_APP });
8748
+ /** Image paths the app reported this session, served to views by ui_read_media. */
8749
+ const media = new MediaRegistry();
8750
+
7945
8751
  server.setRequestHandler(ListToolsRequestSchema, async () => {
7946
- return {
7947
- tools: TOOLS.map((t) => ({
8752
+ const ui = uiEnabled();
8753
+ const tools = TOOLS.map((t) => {
8754
+ const meta = ui ? toolUiMeta(t.name) : undefined;
8755
+ return {
7948
8756
  name: t.name,
7949
8757
  description: t.description,
7950
8758
  // Gemini rejects multi-type fields that JSON Schema allows.
7951
8759
  inputSchema: portableSchema(t.inputSchema),
7952
- })),
7953
- };
8760
+ ...(meta ? { _meta: meta } : {}),
8761
+ };
8762
+ });
8763
+ return { tools: ui ? [...tools, ...APP_TOOLS] : tools };
8764
+ });
8765
+
8766
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
8767
+ resources: uiEnabled() ? listUiResources() : [],
8768
+ }));
8769
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
8770
+ resourceTemplates: [],
8771
+ }));
8772
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
8773
+ const res = readUiResource(request.params.uri, mcpVersion());
8774
+ if (!res) throw new Error(`unknown resource: ${request.params.uri}`);
8775
+ return res;
7954
8776
  });
7955
8777
 
7956
8778
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
8779
+ if (request.params.name === "ui_read_media") {
8780
+ return media.read(request.params.arguments?.paths);
8781
+ }
7957
8782
  const tool = TOOLS.find((t) => t.name === request.params.name);
7958
8783
  if (!tool) {
7959
8784
  return {
@@ -8007,6 +8832,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
8007
8832
  ];
8008
8833
  try {
8009
8834
  const data = result?.data ?? result;
8835
+ media.note(data);
8010
8836
  const previews = collectInlinePreviewPaths(command, data);
8011
8837
  for (const previewPath of previews) {
8012
8838
  const fs = await import("node:fs/promises");