@sync.so/mcp-server 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +261 -86
  2. package/deploy/generation-status/app.html.gz +0 -0
  3. package/deploy/generation-status/release.json +6 -0
  4. package/dist/abort-signals.d.ts +6 -0
  5. package/dist/abort-signals.d.ts.map +1 -0
  6. package/dist/abort-signals.js +23 -0
  7. package/dist/account-features.d.ts +7 -0
  8. package/dist/account-features.d.ts.map +1 -0
  9. package/dist/account-features.js +48 -0
  10. package/dist/app-upload-relay.d.ts +13 -0
  11. package/dist/app-upload-relay.d.ts.map +1 -0
  12. package/dist/app-upload-relay.js +136 -0
  13. package/dist/asset-thumbnail.d.ts +3 -0
  14. package/dist/asset-thumbnail.d.ts.map +1 -0
  15. package/dist/asset-thumbnail.js +2 -0
  16. package/dist/auth/async-context.d.ts +5 -0
  17. package/dist/auth/async-context.d.ts.map +1 -0
  18. package/dist/auth/async-context.js +14 -0
  19. package/dist/auth/bearer-auth.d.ts +16 -0
  20. package/dist/auth/bearer-auth.d.ts.map +1 -0
  21. package/dist/auth/bearer-auth.js +55 -0
  22. package/dist/auth/client-credentials.d.ts +13 -0
  23. package/dist/auth/client-credentials.d.ts.map +1 -0
  24. package/dist/auth/client-credentials.js +67 -0
  25. package/dist/auth/device-auth.d.ts.map +1 -1
  26. package/dist/auth/device-auth.js +3 -2
  27. package/dist/auth/fixture-cleanup.d.ts +4 -0
  28. package/dist/auth/fixture-cleanup.d.ts.map +1 -0
  29. package/dist/auth/fixture-cleanup.js +9 -0
  30. package/dist/auth/index.d.ts +2 -0
  31. package/dist/auth/index.d.ts.map +1 -1
  32. package/dist/auth/index.js +2 -0
  33. package/dist/auth/oauth-diagnostics.d.ts +11 -0
  34. package/dist/auth/oauth-diagnostics.d.ts.map +1 -0
  35. package/dist/auth/oauth-diagnostics.js +78 -0
  36. package/dist/auth/oauth-provider.d.ts +16 -0
  37. package/dist/auth/oauth-provider.d.ts.map +1 -0
  38. package/dist/auth/oauth-provider.js +107 -0
  39. package/dist/auth/resource-metadata.d.ts +2 -0
  40. package/dist/auth/resource-metadata.d.ts.map +1 -0
  41. package/dist/auth/resource-metadata.js +3 -0
  42. package/dist/auth/token-verification.d.ts +11 -0
  43. package/dist/auth/token-verification.d.ts.map +1 -0
  44. package/dist/auth/token-verification.js +99 -0
  45. package/dist/auth/verification-cache.d.ts +8 -0
  46. package/dist/auth/verification-cache.d.ts.map +1 -0
  47. package/dist/auth/verification-cache.js +111 -0
  48. package/dist/chatgpt-analytics.d.ts +22 -0
  49. package/dist/chatgpt-analytics.d.ts.map +1 -0
  50. package/dist/chatgpt-analytics.js +172 -0
  51. package/dist/chatgpt-app.d.ts +15 -0
  52. package/dist/chatgpt-app.d.ts.map +1 -0
  53. package/dist/chatgpt-app.js +133 -0
  54. package/dist/chatgpt-release.d.ts +12 -0
  55. package/dist/chatgpt-release.d.ts.map +1 -0
  56. package/dist/chatgpt-release.js +30 -0
  57. package/dist/cli.js +30 -16
  58. package/dist/client-profile.d.ts +10 -0
  59. package/dist/client-profile.d.ts.map +1 -0
  60. package/dist/client-profile.js +45 -0
  61. package/dist/config.d.ts +17 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +52 -0
  64. package/dist/generation-status-html.d.ts +2 -0
  65. package/dist/generation-status-html.d.ts.map +1 -0
  66. package/dist/generation-status-html.js +26 -0
  67. package/dist/generation-status.d.ts +6 -0
  68. package/dist/generation-status.d.ts.map +1 -0
  69. package/dist/generation-status.js +37 -0
  70. package/dist/http-client.d.ts +45 -1
  71. package/dist/http-client.d.ts.map +1 -1
  72. package/dist/http-client.js +171 -34
  73. package/dist/http-server.d.ts +53 -0
  74. package/dist/http-server.d.ts.map +1 -0
  75. package/dist/http-server.js +753 -0
  76. package/dist/http-session.d.ts +18 -0
  77. package/dist/http-session.d.ts.map +1 -0
  78. package/dist/http-session.js +189 -0
  79. package/dist/index.d.ts +4 -3
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +3 -2
  82. package/dist/oauth-response.d.ts +3 -0
  83. package/dist/oauth-response.d.ts.map +1 -0
  84. package/dist/oauth-response.js +30 -0
  85. package/dist/openapi/index.d.ts +2 -2
  86. package/dist/openapi/index.d.ts.map +1 -1
  87. package/dist/openapi/index.js +1 -1
  88. package/dist/openapi/parser.d.ts +2 -0
  89. package/dist/openapi/parser.d.ts.map +1 -1
  90. package/dist/openapi/parser.js +41 -1
  91. package/dist/openapi/types.d.ts +3 -0
  92. package/dist/openapi/types.d.ts.map +1 -1
  93. package/dist/organization-selection.d.ts +61 -0
  94. package/dist/organization-selection.d.ts.map +1 -0
  95. package/dist/organization-selection.js +142 -0
  96. package/dist/rate-limits.d.ts +13 -0
  97. package/dist/rate-limits.d.ts.map +1 -0
  98. package/dist/rate-limits.js +67 -0
  99. package/dist/revision.d.ts +6 -0
  100. package/dist/revision.d.ts.map +1 -0
  101. package/dist/revision.js +17 -0
  102. package/dist/runtime-diagnostics.d.ts +32 -0
  103. package/dist/runtime-diagnostics.d.ts.map +1 -0
  104. package/dist/runtime-diagnostics.js +58 -0
  105. package/dist/server.d.ts +23 -2
  106. package/dist/server.d.ts.map +1 -1
  107. package/dist/server.js +893 -32
  108. package/dist/session-registry.d.ts +54 -0
  109. package/dist/session-registry.d.ts.map +1 -0
  110. package/dist/session-registry.js +202 -0
  111. package/dist/session-telemetry.d.ts +13 -0
  112. package/dist/session-telemetry.d.ts.map +1 -0
  113. package/dist/session-telemetry.js +87 -0
  114. package/dist/spend-confirmation.d.ts +21 -0
  115. package/dist/spend-confirmation.d.ts.map +1 -0
  116. package/dist/spend-confirmation.js +105 -0
  117. package/dist/tool-analytics-context.d.ts +11 -0
  118. package/dist/tool-analytics-context.d.ts.map +1 -0
  119. package/dist/tool-analytics-context.js +8 -0
  120. package/dist/tool-names.d.ts +35 -0
  121. package/dist/tool-names.d.ts.map +1 -0
  122. package/dist/tool-names.js +133 -0
  123. package/dist/tools/account-context.d.ts +5 -0
  124. package/dist/tools/account-context.d.ts.map +1 -0
  125. package/dist/tools/account-context.js +131 -0
  126. package/dist/tools/account-management.d.ts +4 -0
  127. package/dist/tools/account-management.d.ts.map +1 -0
  128. package/dist/tools/account-management.js +111 -0
  129. package/dist/tools/app-tools.d.ts +31 -0
  130. package/dist/tools/app-tools.d.ts.map +1 -0
  131. package/dist/tools/app-tools.js +350 -0
  132. package/dist/tools/asset-analysis.d.ts +4 -0
  133. package/dist/tools/asset-analysis.d.ts.map +1 -0
  134. package/dist/tools/asset-analysis.js +172 -0
  135. package/dist/tools/asset-creators.d.ts +4 -0
  136. package/dist/tools/asset-creators.d.ts.map +1 -0
  137. package/dist/tools/asset-creators.js +38 -0
  138. package/dist/tools/audio-trim.d.ts +6 -0
  139. package/dist/tools/audio-trim.d.ts.map +1 -0
  140. package/dist/tools/audio-trim.js +79 -0
  141. package/dist/tools/batch-results.d.ts +16 -0
  142. package/dist/tools/batch-results.d.ts.map +1 -0
  143. package/dist/tools/batch-results.js +103 -0
  144. package/dist/tools/batches.d.ts +28 -0
  145. package/dist/tools/batches.d.ts.map +1 -0
  146. package/dist/tools/batches.js +293 -0
  147. package/dist/tools/dialogue.d.ts +6 -0
  148. package/dist/tools/dialogue.d.ts.map +1 -0
  149. package/dist/tools/dialogue.js +309 -0
  150. package/dist/tools/estimate-duration.d.ts +35 -0
  151. package/dist/tools/estimate-duration.d.ts.map +1 -0
  152. package/dist/tools/estimate-duration.js +66 -0
  153. package/dist/tools/feedback.d.ts +14 -0
  154. package/dist/tools/feedback.d.ts.map +1 -0
  155. package/dist/tools/feedback.js +72 -0
  156. package/dist/tools/generated-adapters.d.ts +25 -0
  157. package/dist/tools/generated-adapters.d.ts.map +1 -0
  158. package/dist/tools/generated-adapters.js +143 -0
  159. package/dist/tools/generation-context.d.ts +14 -0
  160. package/dist/tools/generation-context.d.ts.map +1 -0
  161. package/dist/tools/generation-context.js +39 -0
  162. package/dist/tools/generation-continuation.d.ts +4 -0
  163. package/dist/tools/generation-continuation.d.ts.map +1 -0
  164. package/dist/tools/generation-continuation.js +124 -0
  165. package/dist/tools/generation-costs.d.ts +6 -0
  166. package/dist/tools/generation-costs.d.ts.map +1 -0
  167. package/dist/tools/generation-costs.js +67 -0
  168. package/dist/tools/generation-output.d.ts +3 -0
  169. package/dist/tools/generation-output.d.ts.map +1 -0
  170. package/dist/tools/generation-output.js +39 -0
  171. package/dist/tools/generator.d.ts +35 -2
  172. package/dist/tools/generator.d.ts.map +1 -1
  173. package/dist/tools/generator.js +127 -9
  174. package/dist/tools/job-status.d.ts +35 -0
  175. package/dist/tools/job-status.d.ts.map +1 -0
  176. package/dist/tools/job-status.js +92 -0
  177. package/dist/tools/job-wait.d.ts +41 -0
  178. package/dist/tools/job-wait.d.ts.map +1 -0
  179. package/dist/tools/job-wait.js +84 -0
  180. package/dist/tools/jobs.d.ts +3 -0
  181. package/dist/tools/jobs.d.ts.map +1 -0
  182. package/dist/tools/jobs.js +210 -0
  183. package/dist/tools/media-upload.d.ts +27 -0
  184. package/dist/tools/media-upload.d.ts.map +1 -0
  185. package/dist/tools/media-upload.js +247 -0
  186. package/dist/tools/model-options.d.ts +36 -0
  187. package/dist/tools/model-options.d.ts.map +1 -0
  188. package/dist/tools/model-options.js +48 -0
  189. package/dist/tools/organization-choice.d.ts +9 -0
  190. package/dist/tools/organization-choice.d.ts.map +1 -0
  191. package/dist/tools/organization-choice.js +136 -0
  192. package/dist/tools/organizations.d.ts +56 -0
  193. package/dist/tools/organizations.d.ts.map +1 -0
  194. package/dist/tools/organizations.js +235 -0
  195. package/dist/tools/output-schemas.d.ts +54 -0
  196. package/dist/tools/output-schemas.d.ts.map +1 -0
  197. package/dist/tools/output-schemas.js +58 -0
  198. package/dist/tools/overrides.d.ts +3 -0
  199. package/dist/tools/overrides.d.ts.map +1 -1
  200. package/dist/tools/overrides.js +113 -15
  201. package/dist/tools/playback-metadata.d.ts +4 -0
  202. package/dist/tools/playback-metadata.d.ts.map +1 -0
  203. package/dist/tools/playback-metadata.js +51 -0
  204. package/dist/tools/project-destination.d.ts +13 -0
  205. package/dist/tools/project-destination.d.ts.map +1 -0
  206. package/dist/tools/project-destination.js +45 -0
  207. package/dist/tools/project-history.d.ts +6 -0
  208. package/dist/tools/project-history.d.ts.map +1 -0
  209. package/dist/tools/project-history.js +45 -0
  210. package/dist/tools/project-intent.d.ts +8 -0
  211. package/dist/tools/project-intent.d.ts.map +1 -0
  212. package/dist/tools/project-intent.js +87 -0
  213. package/dist/tools/running-submissions.d.ts +7 -0
  214. package/dist/tools/running-submissions.d.ts.map +1 -0
  215. package/dist/tools/running-submissions.js +91 -0
  216. package/dist/tools/speech.d.ts +6 -0
  217. package/dist/tools/speech.d.ts.map +1 -0
  218. package/dist/tools/speech.js +116 -0
  219. package/dist/tools/studio-draft.d.ts +7 -0
  220. package/dist/tools/studio-draft.d.ts.map +1 -0
  221. package/dist/tools/studio-draft.js +195 -0
  222. package/dist/tools/studio-view-contract.d.ts +184 -0
  223. package/dist/tools/studio-view-contract.d.ts.map +1 -0
  224. package/dist/tools/studio-view-contract.js +67 -0
  225. package/dist/tools/studio-view.d.ts +6 -0
  226. package/dist/tools/studio-view.d.ts.map +1 -0
  227. package/dist/tools/studio-view.js +126 -0
  228. package/dist/tools/timeline-lipsync.d.ts +316 -0
  229. package/dist/tools/timeline-lipsync.d.ts.map +1 -0
  230. package/dist/tools/timeline-lipsync.js +304 -0
  231. package/dist/tools/translation-preview.d.ts +8 -0
  232. package/dist/tools/translation-preview.d.ts.map +1 -0
  233. package/dist/tools/translation-preview.js +146 -0
  234. package/dist/tools/translation.d.ts +6 -0
  235. package/dist/tools/translation.d.ts.map +1 -0
  236. package/dist/tools/translation.js +131 -0
  237. package/dist/tools/upload-widget.d.ts +13 -0
  238. package/dist/tools/upload-widget.d.ts.map +1 -0
  239. package/dist/tools/upload-widget.js +788 -0
  240. package/dist/toolsets.d.ts +18 -0
  241. package/dist/toolsets.d.ts.map +1 -0
  242. package/dist/toolsets.js +157 -0
  243. package/dist/update-check.d.ts +38 -0
  244. package/dist/update-check.d.ts.map +1 -0
  245. package/dist/update-check.js +150 -0
  246. package/dist/upload-runtime.d.ts +45 -0
  247. package/dist/upload-runtime.d.ts.map +1 -0
  248. package/dist/upload-runtime.js +156 -0
  249. package/dist/upload-source.d.ts +4 -0
  250. package/dist/upload-source.d.ts.map +1 -0
  251. package/dist/upload-source.js +46 -0
  252. package/dist/version.d.ts +2 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +3 -0
  255. package/package.json +77 -31
  256. package/dist/openapi/parser.test.d.ts +0 -2
  257. package/dist/openapi/parser.test.d.ts.map +0 -1
  258. package/dist/openapi/parser.test.js +0 -188
  259. package/dist/tools/generator.test.d.ts +0 -2
  260. package/dist/tools/generator.test.d.ts.map +0 -1
  261. package/dist/tools/generator.test.js +0 -178
@@ -0,0 +1,350 @@
1
+ import { z } from 'zod';
2
+ import { resolveClientProfile } from '../client-profile.js';
3
+ import { uploadRuntime } from '../upload-runtime.js';
4
+ import { generationContextHeaders, generationContextSchema } from './generation-context.js';
5
+ import { ASSET_TYPE_BY_KIND, assertValidFileParam, fileInput, resolveMedia, uploadMediaAsset, } from './media-upload.js';
6
+ import { modelOptionsSchema, parseModelOptions } from './model-options.js';
7
+ import { generationOutputSchema, uploadMediaOutputSchema } from './output-schemas.js';
8
+ import { assertProjectDestination, createProjectId, newProjectFields, } from './project-destination.js';
9
+ import { createRunningSubmissions } from './running-submissions.js';
10
+ function providedCount(...values) {
11
+ return values.filter(Boolean).length;
12
+ }
13
+ // Picks the source the way resolveMedia does. A ChatGPT file is named by its file_id;
14
+ // its download_url changes on every call.
15
+ function mediaIdentity(url, assetId, file) {
16
+ if (assetId)
17
+ return ['asset', assetId];
18
+ if (url)
19
+ return ['url', url];
20
+ if (file && typeof file === 'object' && 'file_id' in file)
21
+ return ['file', file.file_id];
22
+ return file;
23
+ }
24
+ /**
25
+ * What makes two lipsync_create calls the same request, ignoring keys and new-project
26
+ * names. The client is part of it because each client has its own default project.
27
+ */
28
+ function lipsyncSubmission(args, model, options, client) {
29
+ return [
30
+ client,
31
+ mediaIdentity(args.videoUrl, args.videoAssetId, args.video),
32
+ mediaIdentity(args.imageUrl, args.imageAssetId, args.image),
33
+ mediaIdentity(args.audioUrl, args.audioAssetId, args.audio),
34
+ args.script,
35
+ args.voiceId,
36
+ args.provider,
37
+ args.stability,
38
+ args.similarityBoost,
39
+ model,
40
+ options,
41
+ args.projectId,
42
+ args.continuation,
43
+ ];
44
+ }
45
+ function assertSingleSource(kind, count) {
46
+ if (count > 1) {
47
+ throw new Error(`Provide only one ${kind} source — use either ${kind}Url, ${kind}AssetId, or the uploaded ${kind} file param.`);
48
+ }
49
+ }
50
+ /**
51
+ * Hand-written tools layered on top of the auto-generated ones.
52
+ *
53
+ * `lipsync_create` is a flat convenience wrapper over POST /v2/generate (the
54
+ * auto-generated generations_create takes a nested `input[]` array).
55
+ *
56
+ * Each media input can arrive three ways:
57
+ * - `*Url` string — a hosted/public URL, e.g. the `url` returned by tts_create
58
+ * or an asset URL. This is the only way to chain another tool's output (the
59
+ * model can't synthesise a fileParam object), and it's passed straight to
60
+ * the generation as `url`.
61
+ * - `*AssetId` string — a durable Sync asset created earlier, typically by
62
+ * assets_upload. This keeps uploaded-file handling separate from generation
63
+ * creation when the host supports file params but should not pass the file
64
+ * directly to the generation tool. For public URLs, use the explicit `*Url`
65
+ * fields or the generated assets_create tool instead.
66
+ * - `video`/`image`/`audio` file objects — declared as `openai/fileParams` so
67
+ * ChatGPT can hand a user-uploaded file straight in. These carry a
68
+ * short-lived `download_url`, so we re-host the bytes through the assets
69
+ * pipeline and pass a durable `assetId` (see rehostUpload).
70
+ *
71
+ * The generation can be driven by either audio (`audioUrl`/`audioAssetId`/
72
+ * `audio`) or text (`script` + `voiceId`). Text is sent directly to POST
73
+ * /v2/generate as a text input, avoiding a separate tts_create call.
74
+ */
75
+ export function createAppTools(httpClient, runtime = uploadRuntime, getProfile = () => resolveClientProfile()) {
76
+ const submissions = createRunningSubmissions(httpClient);
77
+ const tools = [
78
+ {
79
+ name: 'assets_upload',
80
+ title: 'Upload media',
81
+ description: 'Stage an image, video, or audio file in Sync asset storage and return a durable assetId. ' +
82
+ 'The file must be a host-provided object with a temporary HTTP download URL. Public URL strings are not accepted. ' +
83
+ 'This creates a media asset but does not start a lipsync generation.',
84
+ inputSchema: {
85
+ mediaType: z
86
+ .enum(['video', 'image', 'audio'])
87
+ .describe('Type of media being uploaded: image, video, or audio.'),
88
+ file: z
89
+ .object(fileInput.shape)
90
+ .describe('Uploaded media file from ChatGPT. Do not pass URL strings here.'),
91
+ },
92
+ outputSchema: uploadMediaOutputSchema,
93
+ annotations: {
94
+ readOnlyHint: false,
95
+ destructiveHint: true,
96
+ idempotentHint: false,
97
+ openWorldHint: true,
98
+ },
99
+ meta: {
100
+ ui: { visibility: ['model', 'app'] },
101
+ 'openai/fileParams': ['file'],
102
+ 'openai/widgetAccessible': true,
103
+ 'openai/toolInvocation/invoking': 'Uploading media to Sync…',
104
+ 'openai/toolInvocation/invoked': 'Media uploaded to Sync.',
105
+ },
106
+ handler: async (args, context) => {
107
+ const signal = context?.signal;
108
+ signal?.throwIfAborted();
109
+ const { mediaType, file } = args;
110
+ if (!mediaType || !['video', 'image', 'audio'].includes(mediaType)) {
111
+ throw new Error('mediaType is required and must be one of: video, image, audio.');
112
+ }
113
+ if (!file) {
114
+ throw new Error('file is required — pass the uploaded ChatGPT file object.');
115
+ }
116
+ if (typeof file === 'string') {
117
+ throw new Error(`Got a string in the file slot ("${file.slice(0, 80)}"). ` +
118
+ 'This tool only accepts uploaded ChatGPT file objects; pass public URLs to assets_create or lipsync_create.');
119
+ }
120
+ const assetId = await uploadMediaAsset(httpClient, mediaType, file, runtime, signal);
121
+ return {
122
+ assetId,
123
+ mediaType,
124
+ assetType: ASSET_TYPE_BY_KIND[mediaType],
125
+ input: { type: mediaType, assetId },
126
+ };
127
+ },
128
+ },
129
+ {
130
+ name: 'lipsync_create',
131
+ title: 'Create lipsync',
132
+ description: 'Use this when the user wants to make a photo talk, make an image or video say a script, or match a speaker’s lip movements to supplied audio. ' +
133
+ 'Create a lipsync video with exactly one visual input (image or video) and one driver (audio or script). ' +
134
+ 'For translating spoken video into another language, use dubbing_create when available. Do not use for subtitles only, standalone audio, or general video editing. ' +
135
+ 'Inputs can be public URLs, Sync asset IDs, or supported host file objects. A script requires a voiceId. ' +
136
+ 'The model defaults to sync-3. New requests create a new project, even when its name matches another project. projectName names the new project; it never selects existing work. Supply projectId only for an explicit user selection or a project just created for this request. Keyed new-project requests require a fresh projectCreationKey, preserved on retries. ' +
137
+ 'Before confirming, estimate using the replacement script or measured audio duration, not the source video length. Script-based quotes are approximate and exclude speech synthesis. ' +
138
+ 'This starts an asynchronous generation and returns its id and status.',
139
+ inputSchema: {
140
+ continuation: generationContextSchema.optional(),
141
+ videoUrl: z
142
+ .string()
143
+ .describe('Public or Sync-hosted video URL. Use exactly one of videoUrl/videoAssetId/video/imageUrl/imageAssetId/image.')
144
+ .optional(),
145
+ videoAssetId: z
146
+ .string()
147
+ .describe('Sync asset id for a video, returned by assets_upload or assets_create. Use exactly one visual input.')
148
+ .optional(),
149
+ imageUrl: z
150
+ .string()
151
+ .describe('Public or Sync-hosted still image URL. Use this for image-to-video lipsync with sync-3.')
152
+ .optional(),
153
+ imageAssetId: z
154
+ .string()
155
+ .describe('Sync asset id for a still image, returned by assets_upload or assets_create. Use this for image-to-video lipsync with sync-3.')
156
+ .optional(),
157
+ audioUrl: z
158
+ .string()
159
+ .describe('Public or Sync-hosted audio URL. Use this when the user supplies audio.')
160
+ .optional(),
161
+ audioAssetId: z
162
+ .string()
163
+ .describe('Sync asset id for audio, returned by assets_upload or assets_create.')
164
+ .optional(),
165
+ script: z.string().describe('Text for the image or video to say.').optional(),
166
+ voiceId: z
167
+ .string()
168
+ .describe('Voice id from voices_list. Required when script is provided.')
169
+ .optional(),
170
+ provider: z
171
+ .string()
172
+ .describe('Voice provider for script-driven lipsync. Defaults to elevenlabs.')
173
+ .optional(),
174
+ stability: z
175
+ .number()
176
+ .describe('Optional ElevenLabs voice stability for script input.')
177
+ .optional(),
178
+ similarityBoost: z
179
+ .number()
180
+ .describe('Optional ElevenLabs similarity boost for script input.')
181
+ .optional(),
182
+ video: z
183
+ .object(fileInput.shape)
184
+ .describe('Uploaded video file from ChatGPT. Use videoUrl for public URLs instead.')
185
+ .optional(),
186
+ image: z
187
+ .object(fileInput.shape)
188
+ .describe('Uploaded still image file from ChatGPT. Use imageUrl for public URLs instead.')
189
+ .optional(),
190
+ audio: z
191
+ .object(fileInput.shape)
192
+ .describe('Uploaded audio file from ChatGPT. Use audioUrl for public URLs instead.')
193
+ .optional(),
194
+ model: z
195
+ .string()
196
+ .describe('Optional model override. Image and video inputs default to sync-3.')
197
+ .optional(),
198
+ options: modelOptionsSchema
199
+ .optional()
200
+ .describe('Model-specific generation preferences. Keep unchanged when retrying the same idempotency key.'),
201
+ idempotencyKey: z
202
+ .string()
203
+ .regex(/^[A-Za-z0-9._~-]{1,128}$/)
204
+ .describe('Unique key for one generation action. Persist and reuse the same key, projectCreationKey (for new projects) or explicit projectId, model, and inputs when retrying after a lost response. Use durable asset IDs or stable URLs; upload files before starting the keyed action. A new intentional generation needs a new key. Passed as the Sync API Idempotency-Key header.')
205
+ .optional(),
206
+ ...newProjectFields,
207
+ projectId: z
208
+ .string()
209
+ .trim()
210
+ .min(1)
211
+ .describe('Existing Sync project ID returned by projects_list or projects_get. Use this to select the same project as the web app. Cannot be combined with projectName.')
212
+ .optional(),
213
+ projectName: z
214
+ .string()
215
+ .trim()
216
+ .min(1)
217
+ .describe('Name for a NEW project only. Never search or reuse a matching name. Cannot be combined with projectId.')
218
+ .optional(),
219
+ },
220
+ outputSchema: generationOutputSchema,
221
+ annotations: {
222
+ readOnlyHint: false,
223
+ destructiveHint: true,
224
+ idempotentHint: false,
225
+ openWorldHint: true,
226
+ },
227
+ meta: {
228
+ 'openai/fileParams': ['video', 'image', 'audio'],
229
+ 'openai/toolInvocation/invoking': 'Creating your lipsync video…',
230
+ 'openai/toolInvocation/invoked': 'Lipsync generation started.',
231
+ },
232
+ handler: async (args, context) => {
233
+ const signal = context?.signal;
234
+ signal?.throwIfAborted();
235
+ const { videoUrl, videoAssetId, imageUrl, imageAssetId, audioUrl, audioAssetId, script, voiceId, provider, stability, similarityBoost, video, image, audio, model, projectName, projectId: requestedProjectId, idempotencyKey, projectCreationKey, } = args;
236
+ const resolvedModel = model ?? 'sync-3';
237
+ const options = parseModelOptions(args.options, {
238
+ model: resolvedModel,
239
+ image: providedCount(imageUrl, imageAssetId, image) > 0,
240
+ });
241
+ if (args.continuation !== undefined) {
242
+ generationContextSchema.parse(args.continuation);
243
+ z.uuid().parse(requestedProjectId);
244
+ }
245
+ // Validate the shape up front, before re-hosting any bytes.
246
+ const audioSourceCount = providedCount(audioUrl, audioAssetId, audio);
247
+ const videoSourceCount = providedCount(videoUrl, videoAssetId, video);
248
+ const imageSourceCount = providedCount(imageUrl, imageAssetId, image);
249
+ assertSingleSource('audio', audioSourceCount);
250
+ assertSingleSource('video', videoSourceCount);
251
+ assertSingleSource('image', imageSourceCount);
252
+ const hasAudio = audioSourceCount > 0;
253
+ const hasScript = Boolean(script);
254
+ const hasVideo = videoSourceCount > 0;
255
+ const hasImage = imageSourceCount > 0;
256
+ if (!hasAudio && !hasScript) {
257
+ throw new Error('Audio or script is required - pass `audioAssetId`, or pass `script` with a `voiceId` from voices_list. lipsync_create also accepts `audioUrl` or an uploaded audio file.');
258
+ }
259
+ if (hasAudio && hasScript) {
260
+ throw new Error('Provide either audio or script, not both.');
261
+ }
262
+ if (hasScript && !voiceId) {
263
+ throw new Error('voiceId is required when using script — call voices_list first.');
264
+ }
265
+ if (!hasVideo && !hasImage) {
266
+ throw new Error('Provide a video or an image - pass `videoAssetId` or `imageAssetId`. lipsync_create also accepts `videoUrl`/`imageUrl` or an uploaded file.');
267
+ }
268
+ if (hasVideo && hasImage) {
269
+ throw new Error('Provide either a video or an image, not both.');
270
+ }
271
+ assertProjectDestination(requestedProjectId, projectName, projectCreationKey);
272
+ // Reject malformed ChatGPT file params before making project or upload requests.
273
+ assertValidFileParam('video', video);
274
+ assertValidFileParam('image', image);
275
+ assertValidFileParam('audio', audio);
276
+ const submit = async () => {
277
+ const projectId = requestedProjectId?.trim() ??
278
+ (await createProjectId(httpClient, {
279
+ name: projectName?.trim() ||
280
+ (context?.clientProfile ?? getProfile()).defaultProjectName,
281
+ projectCreationKey,
282
+ idempotencyKey,
283
+ defaultWorkflow: hasImage ? 'image_to_video' : 'lipsync',
284
+ }, signal));
285
+ if (requestedProjectId !== undefined) {
286
+ // Check access before transferring files. Generation admission checks it again.
287
+ await httpClient.request('get', `/v2/projects/${encodeURIComponent(projectId)}`, {
288
+ signal,
289
+ });
290
+ }
291
+ const lineage = await generationContextHeaders(httpClient, args, ['lipsync', 'image_to_video', 'translate_and_dub'], signal);
292
+ // URLs go through verbatim; uploaded files are re-hosted; assetIds are reused.
293
+ const driver = hasScript
294
+ ? {
295
+ type: 'text',
296
+ provider: {
297
+ name: provider ?? 'elevenlabs',
298
+ voiceId,
299
+ script,
300
+ ...(stability === undefined ? {} : { stability }),
301
+ ...(similarityBoost === undefined ? {} : { similarityBoost }),
302
+ },
303
+ }
304
+ : await resolveMedia(httpClient, 'audio', audioUrl, audioAssetId, audio, runtime, signal);
305
+ const visual = hasImage
306
+ ? await resolveMedia(httpClient, 'image', imageUrl, imageAssetId, image, runtime, signal)
307
+ : await resolveMedia(httpClient, 'video', videoUrl, videoAssetId, video, runtime, signal);
308
+ signal?.throwIfAborted();
309
+ const generation = await httpClient.request('post', '/v2/generate', {
310
+ signal,
311
+ ...(idempotencyKey || Object.keys(lineage).length
312
+ ? {
313
+ headers: {
314
+ ...lineage,
315
+ ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
316
+ },
317
+ }
318
+ : {}),
319
+ body: {
320
+ model: resolvedModel,
321
+ ...(options === undefined ? {} : { options }),
322
+ input: [visual, driver],
323
+ projectId,
324
+ },
325
+ });
326
+ return generation;
327
+ };
328
+ const client = (context?.clientProfile ?? getProfile()).name;
329
+ return submissions.once(lipsyncSubmission(args, resolvedModel, options, client), submit, signal);
330
+ },
331
+ },
332
+ ];
333
+ const lipsync = tools.find((tool) => tool.name === 'lipsync_create');
334
+ // Embedded apps cannot invoke tools annotated with fileParams, even with
335
+ // asset-only arguments. Reuse generation admission/execution without exposing
336
+ // the host's attachment transport to this app-only entry point.
337
+ const inputSchema = { ...lipsync.inputSchema };
338
+ for (const key of ['video', 'image', 'audio', 'videoUrl', 'imageUrl', 'audioUrl']) {
339
+ delete inputSchema[key];
340
+ }
341
+ const { 'openai/fileParams': _fileParams, ...meta } = lipsync.meta ?? {};
342
+ tools.push({
343
+ ...lipsync,
344
+ name: 'lipsync_create-from-assets',
345
+ description: 'Create one lipsync generation from saved Sync image/video and audio asset IDs, or a saved visual asset plus script and voiceId. Uses the same project access, options, and idempotency rules as lipsync_create. For embedded Sync interfaces only.',
346
+ inputSchema,
347
+ meta: { ...meta, ui: { visibility: ['app'] }, 'openai/widgetAccessible': true },
348
+ });
349
+ return tools;
350
+ }
@@ -0,0 +1,4 @@
1
+ import { type HttpClient } from '../http-client.js';
2
+ import type { McpToolDefinition } from './generator.js';
3
+ export declare function createAssetAnalysisTools(http: HttpClient): McpToolDefinition[];
4
+ //# sourceMappingURL=asset-analysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-analysis.d.ts","sourceRoot":"","sources":["../../src/tools/asset-analysis.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AA0MxD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,EAAE,CAE9E"}
@@ -0,0 +1,172 @@
1
+ import { z } from 'zod';
2
+ import { ApiRequestError } from '../http-client.js';
3
+ import { pollUntil, waitInput } from './job-wait.js';
4
+ /**
5
+ * Scene detection and stem separation. Their API routes are internal and behind
6
+ * server-side feature flags, so they are not in the public spec and are written by hand.
7
+ * They are listed only to accounts whose features include them (account-features.ts);
8
+ * a call from any other account still gets the API's 404, reported as not enabled.
9
+ */
10
+ const assetId = z.uuid();
11
+ // A disabled flag answers Nest's bare 404 ({ statusCode, message: 'Not Found' }, no
12
+ // errorCode), and an API without the route answers "Cannot GET …". Coded 404s, such
13
+ // as asset_not_found, keep their own error.
14
+ const MISSING_ROUTE = / - (?:Not Found|Cannot (?:GET|POST) \S+)$/;
15
+ async function whenEnabled(feature, call) {
16
+ try {
17
+ return await call();
18
+ }
19
+ catch (error) {
20
+ if (error instanceof ApiRequestError &&
21
+ error.status === 404 &&
22
+ error.code === undefined &&
23
+ MISSING_ROUTE.test(error.message))
24
+ throw new ApiRequestError(`${feature} isn't enabled for this account yet.`, 404, 'feature_not_enabled');
25
+ throw error;
26
+ }
27
+ }
28
+ const sceneSchema = z.object({
29
+ startFrame: z
30
+ .number()
31
+ .int()
32
+ .nonnegative()
33
+ .describe('First frame of the shot, counted from 0, inclusive.'),
34
+ endFrame: z
35
+ .number()
36
+ .int()
37
+ .nonnegative()
38
+ .describe('Last frame of the shot, inclusive. The next scene starts at endFrame + 1.'),
39
+ startSeconds: z
40
+ .number()
41
+ .nullable()
42
+ .describe('Shot start in seconds from the start of the video (startFrame / fps).'),
43
+ endSeconds: z
44
+ .number()
45
+ .nullable()
46
+ .describe('Shot end in seconds, (endFrame + 1) / fps: the next scene startSeconds. Null when fps is unknown.'),
47
+ });
48
+ const scenesSchema = z.object({
49
+ status: z
50
+ .enum(['not_requested', 'pending', 'completed', 'failed'])
51
+ .describe('completed: scenes holds the shots. pending: detection is running. failed: the last run stored no scenes; call again to retry.'),
52
+ fps: z.number().positive().nullable().describe('Frame rate used for seconds; null if unknown.'),
53
+ scenes: z.array(sceneSchema).describe('Shots in video order. Empty unless completed.'),
54
+ });
55
+ const scenesOutput = {
56
+ assetId: assetId.describe('The VIDEO asset.'),
57
+ ...scenesSchema.shape,
58
+ jobId: z.string().optional().describe('Detection job started by this call, if it started one.'),
59
+ };
60
+ const sceneStart = z.object({
61
+ success: z.boolean(),
62
+ jobId: z.string().optional(),
63
+ reason: z.string().optional(),
64
+ });
65
+ function createScenesTool(http) {
66
+ const input = z.object({ assetId, wait: waitInput });
67
+ return {
68
+ name: 'scenes_detect',
69
+ title: 'Detect scenes in a video',
70
+ description: 'Find the shots in a saved VIDEO asset. A scene is one continuous shot between two camera cuts. Returns scenes in video order, each with startFrame and endFrame (frames from 0, both inclusive; the next scene starts at endFrame + 1) and startSeconds and endSeconds (seconds from the start of the video; endSeconds equals the next scene startSeconds). ' +
71
+ 'Starts detection only when the asset has no stored scenes or the last run failed; otherwise it returns the stored or running result, so calling again never reruns a finished detection. Pass wait: true to wait on the server; while status is pending, call again with wait: true. ' +
72
+ 'Typical use: shot-aware lipsync with lipsync_create-timeline. Give each speaker one clip per shot and start and end clips on scene boundaries, so the lipsynced face never switches mid-shot; clip startFrame and endFrame are inclusive too, so a scene maps onto a clip as is.',
73
+ inputSchema: input.shape,
74
+ outputSchema: scenesOutput,
75
+ annotations: {
76
+ readOnlyHint: false,
77
+ destructiveHint: false,
78
+ idempotentHint: true,
79
+ openWorldHint: false,
80
+ },
81
+ handler: (args, context) => whenEnabled('Scene detection', async () => {
82
+ const { assetId: id, wait } = input.parse(args);
83
+ const read = async (signal = context?.signal) => scenesSchema.parse(await http.request('get', '/v2/asset-metadata/scenes', {
84
+ query: { assetId: id },
85
+ signal,
86
+ }));
87
+ let state = await read();
88
+ let jobId;
89
+ // A new request clears stored scenes, so only start when there are none to keep.
90
+ if (state.status === 'not_requested' || state.status === 'failed') {
91
+ const started = sceneStart.parse(await http.request('post', '/v2/asset-metadata/scene-detection', {
92
+ body: { assetId: id },
93
+ signal: context?.signal,
94
+ }));
95
+ if (!started.success)
96
+ throw new Error(started.reason === 'unsupported_asset_type'
97
+ ? `Asset ${id} is not a video. Scene detection needs a VIDEO asset.`
98
+ : `Scene detection did not start for asset ${id}${started.reason ? `: ${started.reason}` : ''}.`);
99
+ jobId = started.jobId;
100
+ state = { ...state, status: 'pending', scenes: [] };
101
+ }
102
+ if (wait && state.status === 'pending')
103
+ state = await pollUntil(read, (scenes) => scenes.status !== 'pending', context, {
104
+ describe: (scenes) => `scene detection ${scenes.status}`,
105
+ });
106
+ return { assetId: id, ...state, ...(jobId ? { jobId } : {}) };
107
+ }),
108
+ };
109
+ }
110
+ const stemJobSchema = z.object({
111
+ id: z.uuid().describe('Stem separation job ID.'),
112
+ assetId: assetId.describe('The AUDIO or VIDEO asset whose audio is separated.'),
113
+ status: z
114
+ .enum(['pending', 'processing', 'completed', 'failed'])
115
+ .describe('pending: queued. processing: extracting and separating. completed: both stem assets exist. failed: see error; call again to retry.'),
116
+ vocalsAssetId: z
117
+ .uuid()
118
+ .nullable()
119
+ .describe('AUDIO asset with only the voices (speech and singing). Null until completed.'),
120
+ backgroundAssetId: z
121
+ .uuid()
122
+ .nullable()
123
+ .describe('AUDIO asset with everything but the voices: music, ambience and effects in one track. Null until completed.'),
124
+ vocalsUrl: z.string().nullable().describe('URL of the vocals WAV. Null until completed.'),
125
+ backgroundUrl: z.string().nullable().describe('URL of the background WAV. Null until completed.'),
126
+ error: z
127
+ .object({ code: z.string(), message: z.string() })
128
+ .nullable()
129
+ .describe('Why the job failed, such as stem_separation_duration_exceeded. Null otherwise.'),
130
+ createdAt: z.string(),
131
+ updatedAt: z.string(),
132
+ });
133
+ function createStemsTool(http) {
134
+ const input = z.object({ assetId, wait: waitInput });
135
+ return {
136
+ name: 'stems_separate',
137
+ title: 'Separate vocals from background audio',
138
+ description: 'Split the audio of a saved AUDIO or VIDEO asset into two new AUDIO assets: vocals, the voices alone (speech and singing), and background, everything else (music, ambience and effects) in one track. A video has its audio extracted first. Both stems are mono 44.1 kHz WAV with the same length and timing as the source, in seconds, and add back up to it. ' +
139
+ 'Typical use: keep the source soundtrack under a dub. Sync tools do not mix audio, so mix the dubbed voice over backgroundUrl yourself (for example with ffmpeg amix), upload the mix and use it as the lipsync audio. The vocals stem is a clean voice track, for example a voice clone sample. ' +
140
+ 'The source must be in Sync storage; for stem_separation_source_not_hosted, upload the file and separate the new asset. Starts a job only when this asset has none or the last one failed; a running or completed separation is returned as is, so calling again never runs it twice. A job can take several minutes (at most 20). Pass wait: true to wait on the server; while status is pending or processing, call again with wait: true.',
141
+ inputSchema: input.shape,
142
+ outputSchema: stemJobSchema.shape,
143
+ annotations: {
144
+ readOnlyHint: false,
145
+ destructiveHint: false,
146
+ idempotentHint: true,
147
+ openWorldHint: false,
148
+ },
149
+ handler: (args, context) => whenEnabled('Stem separation', async () => {
150
+ const { assetId: id, wait } = input.parse(args);
151
+ const path = `/v2/assets/${id}/separate-stems`;
152
+ const request = async (method, signal = context?.signal) => stemJobSchema.parse(await http.request(method, path, { signal }));
153
+ // Read first: the start route is limited to 60 calls an hour, and polling uses this one.
154
+ let job = await request('get').catch((error) => {
155
+ if (error instanceof ApiRequestError && error.code === 'stem_separation_not_found')
156
+ return undefined;
157
+ throw error;
158
+ });
159
+ if (!job || job.status === 'failed')
160
+ job = await request('post');
161
+ if (wait && !settled(job))
162
+ job = await pollUntil((signal) => request('get', signal), settled, context, {
163
+ describe: (stems) => `stem separation ${stems.status}`,
164
+ });
165
+ return job;
166
+ }),
167
+ };
168
+ }
169
+ const settled = (job) => job.status === 'completed' || job.status === 'failed';
170
+ export function createAssetAnalysisTools(http) {
171
+ return [createScenesTool(http), createStemsTool(http)];
172
+ }
@@ -0,0 +1,4 @@
1
+ import type { HttpClient } from '../http-client.js';
2
+ import type { McpToolDefinition } from './generator.js';
3
+ export declare function createAssetCreatorsTool(http: HttpClient): McpToolDefinition;
4
+ //# sourceMappingURL=asset-creators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-creators.d.ts","sourceRoot":"","sources":["../../src/tools/asset-creators.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAcxD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA4B3E"}
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ const member = z.object({
3
+ userId: z.uuid(),
4
+ user: z
5
+ .object({
6
+ id: z.uuid(),
7
+ displayName: z.string().nullable(),
8
+ email: z.string().nullable(),
9
+ avatarUrl: z.string().nullable(),
10
+ })
11
+ .nullish(),
12
+ });
13
+ export function createAssetCreatorsTool(http) {
14
+ return {
15
+ name: 'assets_list-creators',
16
+ title: 'Read Sync asset creators',
17
+ description: "Read the connected organization's members (display name, email and avatar) for the Sync interface's asset creator filter. Organization scope is resolved from the authenticated connection.",
18
+ // The creator filter needs member emails; keep them out of the model's context.
19
+ meta: { ui: { visibility: ['app'] } },
20
+ inputSchema: {},
21
+ outputSchema: { members: member.array() },
22
+ annotations: {
23
+ readOnlyHint: true,
24
+ destructiveHint: false,
25
+ idempotentHint: true,
26
+ openWorldHint: false,
27
+ },
28
+ handler: async (_args, context) => {
29
+ const identity = z
30
+ .object({ organization: z.object({ id: z.uuid() }) })
31
+ .parse(await http.request('get', '/v2/oauth/account', { signal: context?.signal }));
32
+ const members = member.array().parse(await http.request('get', `/v2/organizations/${identity.organization.id}/members`, {
33
+ signal: context?.signal,
34
+ }));
35
+ return { members };
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { HttpClient } from '../http-client.js';
2
+ import type { ParsedOperation } from '../openapi/types.js';
3
+ import type { McpToolDefinition } from './generator.js';
4
+ export declare function supportsAudioTrimming(operations: ParsedOperation[]): boolean;
5
+ export declare function createAudioTrimTools(operations: ParsedOperation[], http: HttpClient): McpToolDefinition[];
6
+ //# sourceMappingURL=audio-trim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-trim.d.ts","sourceRoot":"","sources":["../../src/tools/audio-trim.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,OAAO,CAS5E;AAUD,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,eAAe,EAAE,EAC7B,IAAI,EAAE,UAAU,GACf,iBAAiB,EAAE,CAmErB"}