@sync.so/mcp-server 0.1.3 → 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 +79 -33
  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,235 @@
1
+ import { z } from 'zod';
2
+ import { ApiRequestError } from '../http-client.js';
3
+ import { currentOrganizationSession, ORGANIZATION_HEADER, OrganizationSession, } from '../organization-selection.js';
4
+ /** GET route listing the caller's organizations (sync-api-v2, CRAFT-7789). */
5
+ export const ORGANIZATIONS_PATH = '/v2/account/organizations';
6
+ const SWITCH_HINT = 'Call organizations_list to see the organizations this connection can act in, then organizations_select with an id to switch this session, or pass organizationId on a single paid create (and its estimate) to run only that call there. A Sync app connection (OAuth) acts only in the organizations ticked when it was connected; to add one, the user reconnects Sync and ticks it.';
7
+ const API_KEY_HINT = 'This connection uses an API key, and API keys are bound to the organization that created them. To work in another organization, use an API key created in it, or sign in to Sync instead of using SYNC_API_KEY.';
8
+ const REFUSED_STEP = 'The session keeps its current organization. Pick an id returned by organizations_list and explain the message to the user; do not retry the same id.';
9
+ // Refusals of x-sync-organization-id (sync-api-v2 #7863). The request never fell back to
10
+ // another organization, so nothing ran.
11
+ const ORGANIZATION_REFUSALS = {
12
+ organization_access_denied: 'Use an organization id from organizations_list, or omit organizationId to use the current one.',
13
+ oauth_organization_not_granted: 'To add it, reconnect the app and tick that organization on the consent screen, keeping the others you still want. Until then, use an organization from organizations_list.',
14
+ api_key_organization_bound: "Omit organizationId, or use an API key created in the other organization's Studio settings.",
15
+ invalid_organization_id: 'Send one organization id from organizations_list.',
16
+ };
17
+ /**
18
+ * Next step for a refused organization: the API's own suggestion (for a connection without
19
+ * a grant, reconnecting and ticking that organization), then what not to do.
20
+ */
21
+ export function organizationNextStep(error) {
22
+ const fallback = error.code ? ORGANIZATION_REFUSALS[error.code] : undefined;
23
+ if (!fallback)
24
+ return undefined;
25
+ const suggestion = error.suggestion
26
+ ? `${error.suggestion} Here, organizations_list lists the organizations this connection can use.`
27
+ : fallback;
28
+ return `${suggestion} Tell the user; retrying the same organization fails the same way.`;
29
+ }
30
+ export function switchHint(credential) {
31
+ return credential === 'api_key' ? API_KEY_HINT : SWITCH_HINT;
32
+ }
33
+ const planSchema = z.object({ id: z.string(), name: z.string() });
34
+ const generationLimitsSchema = z.object({
35
+ concurrency: z.number().finite().nonnegative(),
36
+ maxOutputDurationSeconds: z.number().finite().nonnegative(),
37
+ });
38
+ const accountOrganizationSchema = z.object({
39
+ id: z.uuid(),
40
+ name: z.string().nullable(),
41
+ role: z.string().optional(),
42
+ });
43
+ export const featuresSchema = z
44
+ .array(z.string())
45
+ .describe('Feature flags enabled for this organization. Tools that depend on a flag follow the active organization.');
46
+ /** GET /v2/account `features`: the flag names enabled for this credential, sorted. */
47
+ function enabledFeatures(value) {
48
+ if (!Array.isArray(value))
49
+ return undefined;
50
+ return value.filter((item) => typeof item === 'string').sort();
51
+ }
52
+ /** GET /v2/account, read field by field so one unexpected field never hides the rest. */
53
+ export function parseAccountOverview(value) {
54
+ const record = value && typeof value === 'object' ? value : {};
55
+ const organization = accountOrganizationSchema.safeParse(record.organization).data;
56
+ const plan = planSchema.safeParse(record.plan).data;
57
+ const limits = generationLimitsSchema.safeParse(record.limits).data;
58
+ const features = enabledFeatures(record.features);
59
+ return {
60
+ ...(organization ? { organization } : {}),
61
+ ...(typeof record.credentialType === 'string' ? { credentialType: record.credentialType } : {}),
62
+ ...(plan ? { plan } : {}),
63
+ ...(limits ? { limits } : {}),
64
+ ...(features ? { features } : {}),
65
+ };
66
+ }
67
+ async function readAccount(http, signal, organizationId) {
68
+ return parseAccountOverview(await http.request('get', '/v2/account', {
69
+ signal,
70
+ ...(organizationId ? { headers: { [ORGANIZATION_HEADER]: organizationId } } : {}),
71
+ }));
72
+ }
73
+ export function sessionOf() {
74
+ return currentOrganizationSession() ?? new OrganizationSession();
75
+ }
76
+ export function summary(organization) {
77
+ return { id: organization.id, name: organization.name };
78
+ }
79
+ function describe(organization) {
80
+ if (!organization)
81
+ return 'an unknown organization';
82
+ return organization.name ? `${organization.name} (${organization.id})` : organization.id;
83
+ }
84
+ /** The credential's own organization, read once a minute at most. */
85
+ export async function defaultOrganization(http, session, signal) {
86
+ const cached = session.defaultOrganization();
87
+ if (cached)
88
+ return cached;
89
+ const account = await readAccount(http, signal);
90
+ if (!account.organization)
91
+ throw new Error('Sync did not report the active organization.');
92
+ const organization = summary(account.organization);
93
+ session.rememberDefault(organization);
94
+ return organization;
95
+ }
96
+ export function apiKeyBound(organization) {
97
+ return new Error(`API keys are bound to one organization, and this key belongs to ${describe(organization)}. ${API_KEY_HINT}`);
98
+ }
99
+ /** Reads the account as `organizationId`; refuses unless the API answered for that organization. */
100
+ export async function readAccountAs(http, organizationId, signal) {
101
+ let account;
102
+ try {
103
+ account = await readAccount(http, signal, organizationId);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof ApiRequestError && error.status !== 401)
107
+ error.nextStep = organizationNextStep(error) ?? REFUSED_STEP;
108
+ throw error;
109
+ }
110
+ if (account.organization?.id !== organizationId)
111
+ throw new Error(`Sync did not switch to organization ${organizationId}: the API answered for ${describe(account.organization)}, so this connection cannot choose an organization per request yet. Nothing was changed.`);
112
+ return { ...account, organization: account.organization };
113
+ }
114
+ const organizationListItem = z.object({
115
+ id: z.uuid(),
116
+ name: z.string().nullable(),
117
+ role: z.string(),
118
+ plan: planSchema.optional(),
119
+ isCurrent: z.boolean(),
120
+ });
121
+ // GET /v2/account/organizations: what the credential can act in, current one first.
122
+ const organizationsResponse = z.object({ organizations: z.array(organizationListItem) });
123
+ export const organizationSummarySchema = z.object({ id: z.uuid(), name: z.string().nullable() });
124
+ const listOutput = {
125
+ organizations: z.array(organizationListItem),
126
+ current: organizationSummarySchema
127
+ .nullable()
128
+ .describe('The organization tools in this session run in.'),
129
+ selectedBy: z
130
+ .enum(['session', 'credential'])
131
+ .describe('session: chosen with organizations_select. credential: the default of this sign-in or API key.'),
132
+ howToSwitch: z.string(),
133
+ };
134
+ const selectOutput = {
135
+ organization: accountOrganizationSchema,
136
+ previousOrganization: organizationSummarySchema.nullable(),
137
+ changed: z.boolean(),
138
+ plan: planSchema.optional(),
139
+ generationLimits: generationLimitsSchema
140
+ .optional()
141
+ .describe('Generations that can run at once and the longest output, in seconds.'),
142
+ features: featuresSchema.optional(),
143
+ howToSwitch: z.string(),
144
+ };
145
+ function sameFeatures(left, right) {
146
+ if ((left === undefined) !== (right === undefined))
147
+ return false;
148
+ return (left ?? []).join('\n') === (right ?? []).join('\n');
149
+ }
150
+ function selection(account, previous, howToSwitch) {
151
+ return {
152
+ organization: account.organization,
153
+ previousOrganization: previous ?? null,
154
+ changed: previous?.id !== account.organization.id,
155
+ ...(account.plan ? { plan: account.plan } : {}),
156
+ ...(account.limits ? { generationLimits: account.limits } : {}),
157
+ ...(account.features ? { features: account.features } : {}),
158
+ howToSwitch,
159
+ };
160
+ }
161
+ /** API keys cannot switch; selecting the key's own organization only reports it. */
162
+ async function selectWithApiKey(http, session, organizationId, signal) {
163
+ const stamp = session.featureStamp();
164
+ const account = await readAccount(http, signal);
165
+ if (!account.organization || account.organization.id !== organizationId)
166
+ throw apiKeyBound(account.organization && summary(account.organization));
167
+ session.recordFeatures(account.features, stamp, account.organization.id);
168
+ return selection({ ...account, organization: account.organization }, summary(account.organization), API_KEY_HINT);
169
+ }
170
+ export function createOrganizationTools(http, credential) {
171
+ return [
172
+ {
173
+ name: 'organizations_list',
174
+ title: 'List your Sync organizations',
175
+ description: 'List the Sync organizations this connection can act in, with role and plan, marking the one this session runs in (isCurrent). Use it when the user names an organization or team, or asks where work is created, then switch with organizations_select or pass organizationId on one paid create. A Sync app connection lists the organizations ticked when it was connected; to add another, the user reconnects Sync and ticks it. With an API key, lists only the key’s organization.',
176
+ inputSchema: {},
177
+ outputSchema: listOutput,
178
+ annotations: {
179
+ readOnlyHint: true,
180
+ destructiveHint: false,
181
+ idempotentHint: true,
182
+ openWorldHint: false,
183
+ },
184
+ handler: async (_args, context) => {
185
+ const session = sessionOf();
186
+ // An API key lists only its own organization; it never sends the header.
187
+ const selected = session.selected;
188
+ const organizations = organizationsResponse
189
+ .parse(await http.request('get', ORGANIZATIONS_PATH, { signal: context?.signal }))
190
+ .organizations.map((organization) => ({
191
+ ...organization,
192
+ isCurrent: selected ? organization.id === selected.id : organization.isCurrent,
193
+ }));
194
+ const current = organizations.find((organization) => organization.isCurrent);
195
+ if (current && !selected)
196
+ session.rememberDefault(summary(current));
197
+ return {
198
+ organizations,
199
+ current: current ? summary(current) : null,
200
+ selectedBy: selected ? 'session' : 'credential',
201
+ howToSwitch: switchHint(credential),
202
+ };
203
+ },
204
+ },
205
+ {
206
+ name: 'organizations_select',
207
+ title: 'Switch the Sync organization',
208
+ description: 'Switch this session to another Sync organization the user belongs to; every later call runs there, including paid generations, until the session ends or another switch. Use an id from organizations_list, and only when the user asks to work in that organization. Returns the new organization’s plan, generation limits and feature flags. The API checks access: a Sync app connection can switch only to organizations ticked when it was connected, and otherwise says to reconnect and tick it; API keys are bound to one organization and cannot switch. To run a single generation elsewhere, pass organizationId on that create instead.',
209
+ inputSchema: {
210
+ organizationId: z.uuid().describe('Organization id returned by organizations_list.'),
211
+ },
212
+ outputSchema: selectOutput,
213
+ annotations: {
214
+ readOnlyHint: false,
215
+ destructiveHint: false,
216
+ idempotentHint: true,
217
+ openWorldHint: false,
218
+ },
219
+ handler: async (args, context) => {
220
+ const organizationId = z.uuid().parse(args.organizationId);
221
+ const session = sessionOf();
222
+ if (credential === 'api_key')
223
+ return selectWithApiKey(http, session, organizationId, context?.signal);
224
+ const previous = session.selected ?? session.defaultOrganization();
225
+ const account = await readAccountAs(http, organizationId, context?.signal);
226
+ const previousFeatures = session.activeFeatures();
227
+ session.select(summary(account.organization), account.features);
228
+ // Tools that depend on a feature flag follow the organization; let the client re-list.
229
+ if (previous?.id !== organizationId || !sameFeatures(previousFeatures, account.features))
230
+ context?.toolListChanged?.();
231
+ return selection(account, previous, SWITCH_HINT);
232
+ },
233
+ },
234
+ ];
235
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ export declare const generationOutputSchema: {
3
+ id: z.ZodOptional<z.ZodString>;
4
+ projectId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
5
+ status: z.ZodOptional<z.ZodString>;
6
+ model: z.ZodOptional<z.ZodString>;
7
+ outputMediaUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
8
+ outputUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ generationEstimate: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
10
+ outputDuration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
11
+ error: z.ZodOptional<z.ZodUnknown>;
12
+ errorCode: z.ZodOptional<z.ZodUnknown>;
13
+ };
14
+ /**
15
+ * GET /v2/generations/{id} answers with the API's GenerationDto, whose names differ from
16
+ * the Generation that /v2/generate returns. Keep the shared names, add the DTO's own.
17
+ */
18
+ export declare const generationDtoOutputSchema: {
19
+ id: z.ZodOptional<z.ZodString>;
20
+ projectId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
+ status: z.ZodOptional<z.ZodString>;
22
+ outputMediaUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
23
+ outputUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ generationEstimate: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
25
+ error: z.ZodOptional<z.ZodUnknown>;
26
+ errorCode: z.ZodOptional<z.ZodUnknown>;
27
+ model: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
28
+ modelName: z.ZodOptional<z.ZodString>;
29
+ outputMediaLength: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
30
+ outputDuration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
31
+ potentialError: z.ZodOptional<z.ZodUnknown>;
32
+ };
33
+ export declare const uploadMediaOutputSchema: {
34
+ assetId: z.ZodString;
35
+ mediaType: z.ZodEnum<{
36
+ audio: "audio";
37
+ image: "image";
38
+ video: "video";
39
+ }>;
40
+ assetType: z.ZodEnum<{
41
+ AUDIO: "AUDIO";
42
+ IMAGE: "IMAGE";
43
+ VIDEO: "VIDEO";
44
+ }>;
45
+ input: z.ZodObject<{
46
+ type: z.ZodEnum<{
47
+ audio: "audio";
48
+ image: "image";
49
+ video: "video";
50
+ }>;
51
+ assetId: z.ZodString;
52
+ }, z.core.$strip>;
53
+ };
54
+ //# sourceMappingURL=output-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-schemas.d.ts","sourceRoot":"","sources":["../../src/tools/output-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,eAAO,MAAM,sBAAsB;IACjC,EAAE;IACF,SAAS;IACT,MAAM;IACN,KAAK;IACL,cAAc;IACd,SAAS;IAMT,kBAAkB;IAOlB,cAAc;IACd,KAAK;IACL,SAAS;CACV,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;CAerC,CAAC;AAEF,eAAO,MAAM,uBAAuB;IAClC,OAAO;IACP,SAAS;;;;;IACT,SAAS;;;;;IACT,KAAK;;;;;;;;CAMN,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ const resultUrl = z
3
+ .string()
4
+ .nullable()
5
+ .describe('Signed result URL when the generation is complete, named as in the API (GET /v2/generations/{id}). Copy this exact string verbatim; do not reconstruct, shorten, or edit it.')
6
+ .optional();
7
+ export const generationOutputSchema = {
8
+ id: z.string().describe('Sync generation id.').optional(),
9
+ projectId: z.string().nullable().describe('Sync project containing this generation.').optional(),
10
+ status: z.string().describe('Current generation status.').optional(),
11
+ model: z.string().describe('Model used for the generation.').optional(),
12
+ outputMediaUrl: resultUrl,
13
+ outputUrl: z
14
+ .string()
15
+ .nullable()
16
+ .describe('Deprecated alias of outputMediaUrl, with the same value. Read outputMediaUrl.')
17
+ .meta({ deprecated: true })
18
+ .optional(),
19
+ generationEstimate: z
20
+ .record(z.string(), z.unknown())
21
+ .nullable()
22
+ .optional()
23
+ .describe('Server-provided completion estimate and server clock. Use only for approximate remaining time; do not invent an estimate when absent.'),
24
+ outputDuration: z.number().nullable().describe('Output duration in seconds.').optional(),
25
+ error: z.unknown().optional(),
26
+ errorCode: z.unknown().optional(),
27
+ };
28
+ /**
29
+ * GET /v2/generations/{id} answers with the API's GenerationDto, whose names differ from
30
+ * the Generation that /v2/generate returns. Keep the shared names, add the DTO's own.
31
+ */
32
+ export const generationDtoOutputSchema = {
33
+ ...generationOutputSchema,
34
+ model: z
35
+ .union([z.string(), z.record(z.string(), z.unknown())])
36
+ .describe('Model used for the generation.')
37
+ .optional(),
38
+ modelName: z.string().describe('Name of the model used for the generation.').optional(),
39
+ outputMediaLength: z.number().nullable().describe('Output duration in seconds.').optional(),
40
+ outputDuration: z
41
+ .number()
42
+ .nullable()
43
+ .describe('Deprecated alias of outputMediaLength, with the same value.')
44
+ .meta({ deprecated: true })
45
+ .optional(),
46
+ potentialError: z.unknown().optional(),
47
+ };
48
+ export const uploadMediaOutputSchema = {
49
+ assetId: z.string().describe('Durable Sync asset id for the uploaded file.'),
50
+ mediaType: z.enum(['video', 'image', 'audio']).describe('Uploaded media type.'),
51
+ assetType: z.enum(['VIDEO', 'IMAGE', 'AUDIO']).describe('Sync asset type.'),
52
+ input: z
53
+ .object({
54
+ type: z.enum(['video', 'image', 'audio']),
55
+ assetId: z.string(),
56
+ })
57
+ .describe('Generation input object that can be passed to Sync generation tools.'),
58
+ };
@@ -1,5 +1,8 @@
1
+ import type { z } from 'zod';
1
2
  type ToolOverride = {
3
+ title?: string;
2
4
  description: string;
5
+ outputSchema?: Record<string, z.ZodType>;
3
6
  };
4
7
  export declare function getOverride(toolName: string): ToolOverride | undefined;
5
8
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../src/tools/overrides.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAgCF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE"}
1
+ {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../src/tools/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAI7B,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;CAC1C,CAAC;AA2JF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE"}
@@ -1,27 +1,125 @@
1
+ import { ASSET_THUMBNAIL_INSTRUCTIONS } from '../asset-thumbnail.js';
2
+ import { generationDtoOutputSchema, generationOutputSchema } from './output-schemas.js';
3
+ // Agent-friendly descriptions, keyed by generated tool name
4
+ // (operationIdToToolName). These encode the end-to-end workflow
5
+ // (discover models/voices → upload assets → tts → generate → poll) and tell the
6
+ // agent what to do next, so the tool list reads like a runbook. Endpoints
7
+ // without an entry fall back to the OpenAPI summary.
1
8
  const TOOL_OVERRIDES = {
2
- 'generate_create-generation': {
3
- description: 'Create a lipsync video. Provide video + audio inputs (URLs or asset IDs). Returns a generation ID — poll with get-generation until status is COMPLETED.',
9
+ // --- Speakers to faces (timeline lipsync) ---
10
+ 'transcriptions_get-speaker-analysis': {
11
+ title: 'Read which face belongs to each speaker',
12
+ description: 'Read which face in the video belongs to each transcript speaker, by transcription id. Video transcriptions of your own media are analysed automatically; pass wait: true while status is processing. Each speaker is matched (with an avatar, the evidence and a faceTrack: one box per frame of the file where that face is visible) or uncertain (with a reason). To lipsync only the people talking on camera, give each lipsync_create-timeline clip of a matched speaker the url from transcriptions_slice-speaker-face-track as speaker.faceTrackUrl, and keep (lipsync: false, when available) clips of uncertain speakers or where the slice url is null.',
4
13
  },
5
- 'generate_get-generation': {
6
- description: 'Get generation status/result by ID. Poll until status is COMPLETED. Result includes output video URL.',
14
+ 'transcriptions_analyze-speakers': {
15
+ title: 'Match speakers to faces',
16
+ description: "Start, or retry, the speaker-to-face analysis for a completed video transcription. Pass the transcription's revision and sourceVideoUrl from transcriptions_get. Read transcriptions_get-speaker-analysis first: it usually already exists. Unbilled; up to 2 minutes of video and 8 speakers; 3 calls a minute.",
7
17
  },
8
- 'generations_get-generations': {
9
- description: 'List generations for the current organization. Supports pagination.',
18
+ 'transcriptions_slice-speaker-face-track': {
19
+ title: "Cut a clip's face track",
20
+ description: "Cut a matched speaker's face track to one clip and get a url for lipsync_create-timeline speaker.faceTrackUrl, so that clip lipsyncs exactly that person. Frames are [startFrame, endFrame) at the video's fps: for a timeline clip pass its startFrame and endFrame + 1. url is null when the face is on none of those frames; keep that clip (lipsync: false) or use detect.",
10
21
  },
11
- generations_estimate: {
12
- description: 'Estimate the cost of a generation before creating it. Returns estimated credits.',
22
+ // --- Discovery ---
23
+ models_list: {
24
+ title: 'List Sync models',
25
+ description: 'List Sync models with their type, features and deprecation status. Some listed models serve other workflows, such as appearance editing; use only a non-deprecated lipsync model in lipsync_create. Use a returned model name as-is; do not invent model names.',
26
+ },
27
+ voices_list: {
28
+ title: 'List voices',
29
+ description: 'List available voices: premade ElevenLabs voices plus voices your org has cloned. For "make this image/video say X", use an available returned voice `id` as `voiceId` in lipsync_create with `script` directly. Respect any returned restriction. Use this to find an existing clone before creating another.',
30
+ },
31
+ // --- Assets (upload + manage reusable media) ---
32
+ 'assets_create-upload-url': {
33
+ title: 'Get a media upload URL',
34
+ description: 'Upload step 1: get a presigned URL for a local file. PUT the raw bytes to the returned `uploadUrl` (same Content-Type), then register it with assets_create using the returned `url`. Use this when your media is not already at a public URL. Max 5GB.',
13
35
  },
14
- 'assets_get-all': {
15
- description: 'List all assets (videos, audio files) in the current organization.',
36
+ assets_create: {
37
+ title: 'Register a media asset',
38
+ description: 'Register a direct media URL or import a YouTube, TikTok, Instagram or Facebook video link as a reusable asset. For a social link, pass url, type=VIDEO and the selected projectId. A 409 import_in_progress means the download is running: wait retryAfterMs (15 seconds if absent), then repeat this exact request with the same URL and project. The API reuses the import and returns its saved VIDEO asset when ready. Never use importJobId as an asset ID. Stop on import_failed, import_not_enabled, import_unavailable or import limits and explain the error; do not automatically restart. Returns an asset id for generation inputs or videoAssetId in dubbing_create when available. Import first, then estimate and confirm translation. Link imports have daily limits.',
39
+ },
40
+ assets_list: {
41
+ title: 'List assets',
42
+ description: 'List assets (video/audio/image) in your organization. Use to find an existing assetId. Pass a small `limit` and narrow with `searchQuery`, `types` or `projectId`. ' +
43
+ ASSET_THUMBNAIL_INSTRUCTIONS,
16
44
  },
17
45
  assets_get: {
18
- description: 'Get a specific asset by ID. Returns asset details and URL.',
46
+ title: 'Get an asset',
47
+ description: 'Get one asset by id (details + URL). ' + ASSET_THUMBNAIL_INSTRUCTIONS,
48
+ },
49
+ assets_update: {
50
+ title: 'Update an asset',
51
+ description: 'Rename an asset or change its visibility by id. Change only what the user asked for.',
52
+ },
53
+ assets_delete: {
54
+ title: 'Delete an asset',
55
+ description: 'Delete an asset by id. Use only on an explicit user request, after confirming which asset.',
56
+ },
57
+ assets_count: {
58
+ title: 'Count assets',
59
+ description: 'Count assets matching the same filters as assets_list (project, creator, search text, type). Returns a number, not the assets.',
60
+ },
61
+ 'assets_get-download-url': {
62
+ title: 'Get an asset download URL',
63
+ description: 'Get a download URL for an accessible asset. Return the URL exactly as given; signed URLs must not be edited.',
64
+ },
65
+ // --- Voices (clone + manage) ---
66
+ voices_clone: {
67
+ title: 'Clone a voice',
68
+ description: 'Use this when the user explicitly wants to save a reusable clone of their own voice or a voice they have permission to clone. Confirm the sample and name first; plan limits and provider requirements apply, so do not claim this is free. Pass exactly one of a Sync-hosted `url`, accessible `assetId`, or `sampleUrls` (up to 10 Sync-hosted samples), plus `provider` (elevenlabs) and `name`. Upload/register local samples first. Video samples have audio extracted by the API. Returns a Sync `id`: use that id as `voiceId` in lipsync_create with script, not the provider-specific `voiceId` field. Do not use for Edit Dialogue previews, selecting an existing voice, or designing a voice from a text description. This creates a persistent voice and is not idempotent. On timeout or uncertain failure, list voices and reconcile; never automatically retry. Surface API plan limits, sample errors and duplicate-name errors without retrying or changing accounts.',
69
+ },
70
+ voices_delete: {
71
+ description: 'Delete a cloned voice by id, freeing a clone slot.',
72
+ },
73
+ // --- Text-to-speech ---
74
+ tts_create: {
75
+ description: 'Synthesize standalone speech audio from text. Use only when the user specifically asks for an audio file or voice preview. Do not use this for "make this image/video say X" lipsync requests — use lipsync_create with `script` and `voiceId` directly.',
76
+ },
77
+ // --- Generate + poll ---
78
+ generations_create: {
79
+ description: 'Create a lipsync video. Provide a video input (or an image for sync-3) and an audio input, each by `url` or `assetId`. Returns a generation `id` — call generations_get with `wait: true` and omit `timeout` to use the API default. If still pending, poll the same id; when completed, read `outputMediaUrl` (`outputUrl` is a deprecated alias). To choose which face to sync in a multi-person video, pass `options.active_speaker_detection` (auto_detect, or coordinates + frame_number).',
80
+ outputSchema: generationOutputSchema,
81
+ },
82
+ generations_get: {
83
+ title: 'Get a generation',
84
+ description: 'Get a generation by id. Pass `wait: true` and omit `timeout` to use the API default. If still pending, poll the same generation id again; never resubmit the generation. When status is COMPLETED, copy the exact `structuredContent.outputMediaUrl` string verbatim; signed URLs must not be reconstructed, shortened, or edited. `outputUrl` is a deprecated alias with the same value.',
85
+ outputSchema: generationOutputSchema,
86
+ },
87
+ generations_list: {
88
+ title: 'List generations',
89
+ description: 'List recent generations for your organization to find generation ids or check status. Pass a small `limit`, or narrow with its filters; for the history of one project, prefer projects_list-generations when available.',
90
+ },
91
+ 'generations_estimate-cost': {
92
+ title: 'Estimate generation cost',
93
+ description: 'Estimate Sync cost before generating. For replacement speech, supply model and script (approximate spoken duration), or audioDuration (measured seconds once audio exists). These override duration; do not quote the full source video length for a shorter replacement script. For video cut_off or silence supply videoDuration and the selected syncMode; for images set visualType=image and omit videoDuration. Otherwise supply billable output duration in seconds, optional fps and reasoningEnabled. Explain returned durationEstimate, label script estimates approximate, and disclose that speech synthesis is excluded. If the schema supports workflow=translate-and-dub, use it for both stages and show the returned breakdown; disclose excluded external provider charges. Otherwise this prices lip-sync only. Does not create jobs or reserve credits.',
94
+ },
95
+ 'generations_get-details': {
96
+ description: 'Get a generation by id (organization-scoped). Returns status and, when COMPLETED, `outputMediaUrl` (`outputUrl` is a deprecated alias).',
97
+ outputSchema: generationDtoOutputSchema,
98
+ },
99
+ generations_delete: {
100
+ description: 'Delete a generation by id. Only terminal generations (COMPLETED/FAILED/REJECTED) can be deleted — deleting one that is still processing returns 409.',
101
+ },
102
+ generations_estimate: {
103
+ description: 'Estimate generation cost before creating it. For replacement speech supply script for an approximate spoken-duration quote, or measured audioDuration once audio exists, instead of source video duration. For video cut_off or silence supply videoDuration and the selected syncMode; for images set visualType=image and omit videoDuration. Explain returned durationEstimate; speech synthesis is excluded. For translate-and-dub use source duration and disclose excluded external provider charges.',
104
+ },
105
+ // --- Projects (group generations + assets) ---
106
+ projects_create: {
107
+ title: 'Create a project',
108
+ description: 'Create a project to group related generations and assets. Pass the returned `id` as `projectId` to lipsync_create or assets_create so they show up together in Studio. Use by default for a new workflow request, even if an existing project has the same name. Supply a fresh UUID id and preserve it on retries. Project names never select existing work.',
109
+ },
110
+ projects_list: {
111
+ title: 'List projects',
112
+ description: 'Search and paginate your accessible Sync projects. Each result includes recent generations and assets, so pass a small `limit` and use `searchQuery` only when the user explicitly refers to an existing project. Workflow names are not project references. Never choose the first match when names are ambiguous. Use the returned id as projectId in lipsync_create to work in the same project as the Sync web app.',
113
+ },
114
+ projects_get: {
115
+ title: 'Get a project',
116
+ description: 'Get one accessible project by id. For its generation history, use projects_list-generations when available.',
19
117
  },
20
- 'models_get-public': {
21
- description: 'List all available lipsync models. Use this to discover model IDs for generation.',
118
+ projects_update: {
119
+ description: "Update a project's name, description, visibility, or mode.",
22
120
  },
23
- 'generate_get-generations': {
24
- description: 'List recent generations. Use to find generation IDs or check batch status.',
121
+ projects_delete: {
122
+ description: 'Delete a project. Generations and assets attached to it are not deleted — they just stop being grouped under it.',
25
123
  },
26
124
  };
27
125
  export function getOverride(toolName) {
@@ -0,0 +1,4 @@
1
+ import type { HttpClient } from '../http-client.js';
2
+ import type { McpToolDefinition } from './generator.js';
3
+ export declare function createPlaybackMetadataTool(http: HttpClient): McpToolDefinition;
4
+ //# sourceMappingURL=playback-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-metadata.d.ts","sourceRoot":"","sources":["../../src/tools/playback-metadata.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAkBxD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAoC9E"}
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ // Narrow read projection of the API's AssetMetadataSchema. Analysis, transcript,
3
+ // provider and face-detection fields never cross the app boundary.
4
+ const playbackMetadata = z.object({
5
+ assetId: z.uuid(),
6
+ duration: z.number().finite().nonnegative().nullish(),
7
+ fps: z.number().finite().positive().nullish(),
8
+ frameCount: z.number().int().nonnegative().nullish(),
9
+ width: z.number().int().positive().nullish(),
10
+ height: z.number().int().positive().nullish(),
11
+ codec: z.string().nullish(),
12
+ format: z.string().nullish(),
13
+ proxyPath: z.url().nullish(),
14
+ proxyRequested: z.boolean().optional(),
15
+ thumbnailsManifestUrl: z.url().nullish(),
16
+ });
17
+ export function createPlaybackMetadataTool(http) {
18
+ const input = z.object({ assetId: z.uuid() }).strict();
19
+ return {
20
+ name: 'assets_get-playback-metadata',
21
+ title: 'Read video playback metadata',
22
+ description: 'Read timing, frame rate, dimensions and playback proxy metadata for an accessible Sync asset. Requires a connected user; API keys return unsupported. Missing metadata stays unknown. Does not analyze media, read transcripts, or modify assets.',
23
+ inputSchema: input.shape,
24
+ outputSchema: { supported: z.boolean(), metadata: playbackMetadata.nullable() },
25
+ annotations: {
26
+ readOnlyHint: true,
27
+ destructiveHint: false,
28
+ idempotentHint: true,
29
+ openWorldHint: false,
30
+ },
31
+ handler: async (args, context) => {
32
+ const { assetId } = input.parse(args);
33
+ const identity = z
34
+ .object({ account: z.object({ id: z.uuid() }).nullable() })
35
+ .parse(await http.request('get', '/v2/oauth/account', { signal: context?.signal }));
36
+ if (!identity.account)
37
+ return { supported: false, metadata: null };
38
+ const rows = playbackMetadata
39
+ .array()
40
+ .max(1)
41
+ .parse(await http.request('get', '/v2/asset-metadata/by-asset', {
42
+ query: { assetId },
43
+ signal: context?.signal,
44
+ }));
45
+ const metadata = rows[0] ?? null;
46
+ if (metadata && metadata.assetId !== assetId)
47
+ throw new Error('Playback metadata does not match the requested asset.');
48
+ return { supported: true, metadata };
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import type { HttpClient } from '../http-client.js';
3
+ export declare const newProjectFields: {
4
+ projectCreationKey: z.ZodOptional<z.ZodUUID>;
5
+ };
6
+ export declare function assertProjectDestination(projectId?: string, projectName?: string, projectCreationKey?: string): void;
7
+ export declare function createProjectId(http: HttpClient, { name, projectCreationKey, idempotencyKey, defaultWorkflow, }: {
8
+ name: string;
9
+ projectCreationKey?: string;
10
+ idempotencyKey?: string;
11
+ defaultWorkflow: 'lipsync' | 'image_to_video' | 'translate_and_dub';
12
+ }, signal?: AbortSignal): Promise<string>;
13
+ //# sourceMappingURL=project-destination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-destination.d.ts","sourceRoot":"","sources":["../../src/tools/project-destination.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,eAAO,MAAM,gBAAgB;IAC3B,kBAAkB;CAMnB,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,IAAI,CAQN;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,UAAU,EAChB,EACE,IAAI,EACJ,kBAAkB,EAClB,cAAc,EACd,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,SAAS,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;CACrE,EACD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ export const newProjectFields = {
4
+ projectCreationKey: z
5
+ .uuid()
6
+ .optional()
7
+ .describe('Fresh random UUID identifying this NEW project creation, not a project ID. Required for keyed generation without projectId. Persist this key, name and generation key on retries; the server derives a separate project UUID.'),
8
+ };
9
+ export function assertProjectDestination(projectId, projectName, projectCreationKey) {
10
+ if (projectId !== undefined && !projectId.trim())
11
+ throw new Error('projectId must not be empty.');
12
+ if (projectId !== undefined && projectName !== undefined)
13
+ throw new Error('Provide either projectId or projectName, not both.');
14
+ if (projectId !== undefined && projectCreationKey !== undefined)
15
+ throw new Error('Provide either projectId or projectCreationKey, not both.');
16
+ if (projectName !== undefined && !projectName.trim())
17
+ throw new Error('projectName must not be empty.');
18
+ }
19
+ export async function createProjectId(http, { name, projectCreationKey, idempotencyKey, defaultWorkflow, }, signal) {
20
+ // Clients holding a tool list from before projectCreationKey existed still send keyed
21
+ // requests. They get an unkeyed new project; only a lost-response retry can then conflict.
22
+ let id;
23
+ if (projectCreationKey !== undefined) {
24
+ z.uuid().parse(projectCreationKey);
25
+ // A creation key cannot be substituted with an existing project's UUID.
26
+ const bytes = createHash('sha256')
27
+ .update('sync:mcp:new-project:v1\0')
28
+ .update(projectCreationKey.toLowerCase())
29
+ .update('\0')
30
+ .update(idempotencyKey ?? '')
31
+ .digest();
32
+ bytes[6] = (bytes[6] & 0x0f) | 0x80;
33
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
34
+ const hex = bytes.subarray(0, 16).toString('hex');
35
+ id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
36
+ }
37
+ signal?.throwIfAborted();
38
+ const project = z.object({ id: z.string().min(1) }).parse(await http.request('post', '/v2/projects', {
39
+ signal,
40
+ body: { name, defaultWorkflow, ...(id ? { id } : {}) },
41
+ }));
42
+ if (id && project.id !== id)
43
+ throw new Error('Sync returned a different project than the one requested.');
44
+ return project.id;
45
+ }