@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
package/dist/server.js CHANGED
@@ -1,51 +1,913 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { z } from 'zod';
3
+ import { isFeatureGatedTool, listedForAccount } from './account-features.js';
4
+ import { relayUploadTool } from './app-upload-relay.js';
5
+ import { ASSET_THUMBNAIL_INSTRUCTIONS } from './asset-thumbnail.js';
2
6
  import { createApiKeyAuth } from './auth/api-key.js';
3
7
  import { performDeviceAuth } from './auth/device-auth.js';
8
+ import { resourceMetadataUrl } from './auth/resource-metadata.js';
4
9
  import { loadToken } from './auth/token-store.js';
5
- import { createHttpClient } from './http-client.js';
10
+ import { createAnalyticsTool, toolAnalyticsSupport, trackToolExecution, uiAnalyticsSchema, } from './chatgpt-analytics.js';
11
+ import { createOpenSyncAppTool, loadChatgptAppReleases } from './chatgpt-app.js';
12
+ import { resolveClientProfile, UNSUPPORTED_UPLOAD_MESSAGE, } from './client-profile.js';
13
+ import { GENERATION_STATUS_HTML } from './generation-status-html.js';
14
+ import { GENERATION_STATUS_METADATA, GENERATION_STATUS_URI, withGenerationStatusCard, } from './generation-status.js';
15
+ import { ApiRequestError, createHttpClient } from './http-client.js';
6
16
  import { fetchSpec } from './openapi/fetcher.js';
7
17
  import { parseSpec } from './openapi/parser.js';
8
- import { generateTools } from './tools/generator.js';
9
- export async function createSyncMcpServer(config) {
10
- const log = (message) => {
11
- process.stderr.write(message);
18
+ import { currentOrganizationSession, OrganizationSession, runInOrganization, runInOrganizationSession, } from './organization-selection.js';
19
+ import { applyEdits, DESCRIPTION_EDITS, INSTRUCTION_EDITS, UNATTENDED_INSTRUCTIONS_PREFIX, withoutSpendConfirmation, } from './spend-confirmation.js';
20
+ import { nameTakenByRenamedTool, oldNameAlias, previousToolName, renamedToolNotice, withLegacyAppDescriptors, } from './tool-names.js';
21
+ import { createAccountContextTool } from './tools/account-context.js';
22
+ import { createAccountManagementTools } from './tools/account-management.js';
23
+ import { createAppTools } from './tools/app-tools.js';
24
+ import { createAssetAnalysisTools } from './tools/asset-analysis.js';
25
+ import { createAssetCreatorsTool } from './tools/asset-creators.js';
26
+ import { createAudioTrimTools, supportsAudioTrimming } from './tools/audio-trim.js';
27
+ import { BATCH_TOOLS, createBatchTools, isBatchOperation } from './tools/batches.js';
28
+ import { createDialogueTools, dialogueOperations } from './tools/dialogue.js';
29
+ import { createFeedbackTools, FEEDBACK_INSTRUCTIONS, feedbackOperation } from './tools/feedback.js';
30
+ import { adaptGeneratedTool } from './tools/generated-adapters.js';
31
+ import { createGenerationContinuationTool } from './tools/generation-continuation.js';
32
+ import { createGenerationCostsTools, generationCostsOperation } from './tools/generation-costs.js';
33
+ import { withGenerationOutputAliases } from './tools/generation-output.js';
34
+ import { derivedToolName, generateTools, operationIdToToolName } from './tools/generator.js';
35
+ import { progressSender } from './tools/job-wait.js';
36
+ import { createJobTools } from './tools/jobs.js';
37
+ import { withOrganizationChoice } from './tools/organization-choice.js';
38
+ import { createOrganizationTools, organizationNextStep, } from './tools/organizations.js';
39
+ import { createPlaybackMetadataTool } from './tools/playback-metadata.js';
40
+ import { createProjectHistoryTools } from './tools/project-history.js';
41
+ import { withProjectIntent } from './tools/project-intent.js';
42
+ import { createSpeechTools, speechOperations } from './tools/speech.js';
43
+ import { createStudioDraftTools, supportsStudioDrafts } from './tools/studio-draft.js';
44
+ import { createStudioViewTools, supportsStudioViews } from './tools/studio-view.js';
45
+ import { createTimelineLipsyncTool, timelineClipLimit, timelineKeepsClips, } from './tools/timeline-lipsync.js';
46
+ import { createTranslationPreviewTools, translationPreviewOperations, } from './tools/translation-preview.js';
47
+ import { createTranslationTools, translationOperation } from './tools/translation.js';
48
+ import { createUploadWidgetTool, registerUploadWidgetResource } from './tools/upload-widget.js';
49
+ import { toolsetIncludes, toolsetInstruction } from './toolsets.js';
50
+ import { updateInstruction, updateToolNotice, } from './update-check.js';
51
+ import { PACKAGE_VERSION } from './version.js';
52
+ const SERVER_DESCRIPTION = 'Sync makes photos talk, lip-syncs videos to audio or a script, translates and dubs videos into another language, and edits spoken words while keeping the original speaker voice. ' +
53
+ 'Use existing media or upload your own. Check available workflows and estimated generation costs before creating a video.';
54
+ // Paragraphs marked appOnly apply only inside the ChatGPT app and its Studio bridge.
55
+ const INSTRUCTION_PARTS = [
56
+ {
57
+ text: 'When helping with Sync, use concise, friendly, lowercase prose. Preserve the exact case of URLs, IDs, code, filenames and quoted user text; follow explicit user style requests. ',
58
+ },
59
+ {
60
+ text: 'After successfully answering or completing the request, optionally suggest at most one useful next step tied to the film or project the user has described. Suggest only capabilities supported by tools available in this connection, skip generic or repeated promotions, and do not perform the suggested action unless the user requests it. ',
61
+ },
62
+ { text: ASSET_THUMBNAIL_INSTRUCTIONS },
63
+ {
64
+ text: 'New workflow requests always start a NEW project. Workflow labels such as Translate and Dub are actions, never project references. Never search for or reuse a project by matching its name, or modify the active project merely because it is open. Use an existing project only when the user explicitly requests an update to that project or selects it in the interface; clarify an ambiguous existing-project reference before any write. For social URLs, first create a fresh project with projects_create, a fresh UUID id and the matching defaultWorkflow (translate_and_dub for translation), then use that returned projectId throughout import and generation with projectIntent=explicit. Persist the UUID and reuse it only for retries of this same creation. A project just created for this request is an intentional destination. For new lipsync_create or dubbing_create calls with saved assets, omit projectId and supply a fresh projectCreationKey, preserving it on retries. Other new workflows and preparation tools require projects_create first, with the matching defaultWorkflow, then the returned projectId and projectIntent=explicit. ',
65
+ },
66
+ {
67
+ text: 'Every call runs in one Sync organization, and paid work is created and billed there. account_get-context names it and says how it was chosen. When organizations_list is available and the user names an organization or team, list the organizations and use the matching id; ask if several match. To work in another organization for the rest of the conversation, call organizations_select. To run one paid create in another organization without switching, pass organizationId on that create and on its estimate; reads of the ids it returns stay in that organization. Name the organization when confirming paid work if the user belongs to more than one. A Sync app connection acts only in the organizations ticked when it was connected; when a switch is refused for that, tell the user to reconnect Sync and tick that organization. API keys are bound to one organization and cannot switch. ',
68
+ },
69
+ {
70
+ text: 'For what a past generation actually cost, use generations_get-costs when available. Find the generation through history if its id is unknown, and ask which one if multiple match. Report dubbing and lip sync separately, credit face value rather than cash paid, and excluded costs. A null total means the full cost is unknown; show knownSubtotalUsd only as a partial amount. Never substitute generations_estimate-cost or start a paid generation to answer a past-cost question. ',
71
+ },
72
+ {
73
+ text: 'In ChatGPT, default new lipsync and image-to-video requests to sync-3, the latest lipsync model, unless the user explicitly chooses another model. Preserve the selected model on an existing draft or an idempotent retry. ',
74
+ appOnly: true,
75
+ },
76
+ {
77
+ text: 'When voices_clone is available, use it only for an explicitly requested reusable voice clone from a sample the user owns or has permission to clone. Confirm the sample and voice name, explain plan/provider restrictions, and do not claim cloning is free. Use the returned id (not the provider voiceId) for lipsync_create with script. After an uncertain clone response, list voices and reconcile before considering another write; never automatically repeat cloning. ',
78
+ },
79
+ {
80
+ text: 'When audio-trims_create is available, offer prefix audio preparation for an over-limit lip-sync request, then re-quote the saved asset. Preparation is separate from paid generation. Video partial lip-sync uses cut_off and preserves the original media. ',
81
+ },
82
+ {
83
+ text: 'Do not offer Sync video workflows for subtitle-only output, text translation, standalone MP3 voiceovers, or timeline editing such as trimming and mixing background music. Explain the supported scope; only offer another capability if a corresponding tool is actually available. ',
84
+ },
85
+ {
86
+ text: 'Edit Dialogue supports targeted edits/removals of existing timed transcript words, with an audio preview in the original speaker voice followed by dialogue-videos_create. It does not support replacing every line with an arbitrary new script. Explain this limit when whole-script replacement is requested; do not promise it or bypass it by splitting the new script into word edits. Do not substitute full-script TTS. Use the dedicated dialogue tools when available; if a required tool is unavailable, explain that the workflow cannot be completed in this connection. ',
87
+ },
88
+ {
89
+ text: 'For a request such as “Translate this YouTube link to Spanish”, first call assets_create with the social URL, type=VIDEO and the selected projectId. YouTube, TikTok, Instagram and Facebook video links are imported into saved assets. A 409 import_in_progress means the import is running: wait retryAfterMs (15 seconds if absent), then repeat the identical assets_create request until it returns an asset. Keep the URL and project unchanged; do not submit a generation while importing or treat importJobId as an asset ID. On import_failed, import_not_enabled, import_unavailable or an import limit error, explain the returned reason and stop; offer a file upload instead of repeatedly starting imports. Once ready, estimate and confirm the paid translation before calling dubbing_create when available with the returned asset id as videoAssetId. ',
90
+ },
91
+ {
92
+ text: 'When dubbing_create is available, use it for translating speech and lip-syncing a saved video. Before confirming the paid action, use generations_estimate-cost with workflow=translate-and-dub if its schema supports that option. Show the combined Sync estimate and disclose excluded external provider charges. Without that option or the returned breakdown, the estimate covers lip-sync only; do not present it as the full workflow cost. Confirm the target language and configuration. Reuse its generation id for status polling. ',
93
+ },
94
+ {
95
+ text: 'When standalone translation-preview tools are available, the app can translate saved audio before final lip-sync. Confirm the source audio, language and quote before translation-previews_create. Persist one UUID key and immutable request; poll the same job, and treat recoveryRequired as unresolved acceptance. Never redub because attaching the output failed. After a translated audio preview is saved, estimate and confirm a separate lipsync_create-from-assets using that audio; do not send it through dubbing_create again. ',
96
+ },
97
+ {
98
+ text: 'When speech-previews_create is available, the app can synthesize a standalone audio preview for its video input. Confirm script, voice and estimated cost. Persist and reuse its idempotencyKey after uncertain admission, poll the same returned job, and never synthesize again because asset attachment failed. ',
99
+ },
100
+ {
101
+ text: 'When Studio publishes a draftId, use studio-drafts_get and studio-drafts_update to change its selections/settings. Chat prose does not select media in Studio. Use the current expectedRevision and confirm UI application only after appliedRevision equals revision. These tools do not start paid work. ',
102
+ appOnly: true,
103
+ },
104
+ {
105
+ text: 'When Studio publishes a viewId, read studio-views_get, then use studio-views_open-project to open a project, studio-views_refresh-project after a rename, studio-views_refresh-assets after uploads, studio-views_show-generation to display an accepted job/result, or studio-views_continue-generation for an explicitly requested continuation. Only an applied receipt confirms UI display. Never repeat backend or paid work to refresh the UI. ',
106
+ appOnly: true,
107
+ },
108
+ {
109
+ text: 'Before quoting script-driven lipsync, call generations_estimate-cost with the actual replacement script when its schema supports script; use measured audioDuration if the replacement audio already exists. Explain the returned durationEstimate and label script-based pricing approximate until synthesis determines the actual length. Never substitute the full source video duration for replacement speech. Disclose speech synthesis separately. If those inputs are unavailable, estimate spoken duration from the script and label it approximate; do not present a source-video quote as the replacement-script price. Re-estimate after script, audio, model or sync-mode changes. ',
110
+ },
111
+ {
112
+ text: 'lipsync_create accepts exactly one visual input (image or video) and one driver (audio or script). For script, call voices_list and select an actual returned voiceId. Public/Sync-hosted media URLs and existing Sync asset IDs in the same organization are supported. For local media, use the Sync interface upload action, or request assets_create-upload-url, PUT the file bytes with its Content-Type, and register the returned URL with assets_create. Use assets_create for public URL imports and projects_create to create a project. The tool defaults to sync-3 and a NEW project. projectName names a new project and never reuses an existing one. Prefer a projectId returned by projects_list when selecting an existing project. When available, use projects_list-generations for history within that project; generations_list is the organization feed. Create once, then poll generations_get by the returned id with wait: true, omitting timeout to use the API default; if still pending, poll that same id rather than creating again. When COMPLETED, return the exact structuredContent.outputMediaUrl verbatim, preserving signed query parameters; outputUrl is a deprecated alias with the same value.',
113
+ },
114
+ ];
115
+ export const SERVER_INSTRUCTIONS = INSTRUCTION_PARTS.map((part) => part.text).join('');
116
+ const MODEL_ONLY_INSTRUCTIONS = INSTRUCTION_PARTS.filter((part) => !part.appOnly)
117
+ .map((part) => part.text)
118
+ .join('');
119
+ export function serverInstructions(profile, spendConfirmation = true, feedback = false, update, toolsetNote = '') {
120
+ const instructions = (profile.supportsAppUi ? SERVER_INSTRUCTIONS : MODEL_ONLY_INSTRUCTIONS) +
121
+ (feedback && profile.name !== 'chatgpt' ? ` ${FEEDBACK_INSTRUCTIONS.trimEnd()}` : '') +
122
+ (toolsetNote ? ` ${toolsetNote}` : '') +
123
+ (update ? ` ${updateInstruction(update)}` : '');
124
+ return spendConfirmation
125
+ ? instructions
126
+ : UNATTENDED_INSTRUCTIONS_PREFIX + applyEdits(instructions, INSTRUCTION_EDITS);
127
+ }
128
+ export function serverDescription(spendConfirmation = true) {
129
+ return spendConfirmation ? SERVER_DESCRIPTION : applyEdits(SERVER_DESCRIPTION, DESCRIPTION_EDITS);
130
+ }
131
+ const TOOL_SECURITY_SCHEMES = [{ type: 'oauth2', scopes: [] }];
132
+ const APP_UI_TOOLS = new Set([
133
+ 'analytics_track-event',
134
+ 'app_open',
135
+ 'studio-drafts_save',
136
+ 'studio-drafts_ack',
137
+ 'studio-views_register',
138
+ 'studio-views_ack',
139
+ ]);
140
+ export const HOSTED_HTTP_TOOL_ALLOWLIST = new Set([
141
+ 'analytics_track-event',
142
+ 'studio-views_get',
143
+ 'studio-views_open-project',
144
+ 'studio-views_refresh-project',
145
+ 'studio-views_refresh-assets',
146
+ 'studio-views_show-generation',
147
+ 'studio-views_continue-generation',
148
+ 'studio-views_register',
149
+ 'studio-views_ack',
150
+ 'studio-drafts_get',
151
+ 'studio-drafts_update',
152
+ 'studio-drafts_save',
153
+ 'studio-drafts_ack',
154
+ 'account_get-usage',
155
+ 'generations_get-costs',
156
+ 'projects_rename',
157
+ 'account_get-context',
158
+ 'organizations_list',
159
+ 'organizations_select',
160
+ 'assets_list-creators',
161
+ 'assets_get-playback-metadata',
162
+ 'generations_get-continuation',
163
+ 'speech-previews_get-options',
164
+ 'speech-previews_create',
165
+ 'speech-previews_get',
166
+ 'dialogue_get-options',
167
+ 'dialogue_upload-source',
168
+ 'dialogue-transcriptions_create',
169
+ 'dialogue-transcriptions_get',
170
+ 'dialogue-previews_create',
171
+ 'dialogue-previews_get',
172
+ 'dialogue-videos_estimate',
173
+ 'dialogue-videos_create',
174
+ 'app_open',
175
+ 'assets_open-upload-widget',
176
+ 'assets_upload',
177
+ 'lipsync_create',
178
+ 'lipsync_create-from-assets',
179
+ 'lipsync_create-timeline',
180
+ 'feedback_send',
181
+ 'dubbing_create',
182
+ 'dubbing_get-options',
183
+ 'translation-previews_estimate',
184
+ 'translation-previews_create',
185
+ 'translation-previews_get',
186
+ 'audio-trims_create',
187
+ 'audio-trims_get',
188
+ 'voices_list',
189
+ 'voices_clone',
190
+ 'generations_get',
191
+ 'models_list',
192
+ 'projects_list',
193
+ 'projects_get',
194
+ 'assets_list',
195
+ 'assets_count',
196
+ 'assets_get',
197
+ 'assets_update',
198
+ 'assets_delete',
199
+ 'assets_get-download-url',
200
+ 'assets_create-upload-url',
201
+ 'assets_create',
202
+ 'projects_create',
203
+ 'generations_list',
204
+ 'projects_list-generations',
205
+ 'generations_estimate-cost',
206
+ // Hidden from the ChatGPT app listing in isToolVisible.
207
+ 'batches_create',
208
+ 'batches_get',
209
+ 'batches_list',
210
+ // Listed only to accounts with the feature; see account-features.ts.
211
+ 'scenes_detect',
212
+ 'stems_separate',
213
+ ]);
214
+ const WIDGET_CALLABLE_HOSTED_TOOLS = new Set([
215
+ 'analytics_track-event',
216
+ 'studio-views_get',
217
+ 'studio-views_open-project',
218
+ 'studio-views_refresh-project',
219
+ 'studio-views_refresh-assets',
220
+ 'studio-views_show-generation',
221
+ 'studio-views_continue-generation',
222
+ 'studio-views_register',
223
+ 'studio-views_ack',
224
+ 'studio-drafts_get',
225
+ 'studio-drafts_update',
226
+ 'studio-drafts_save',
227
+ 'studio-drafts_ack',
228
+ 'account_get-usage',
229
+ 'generations_get-costs',
230
+ 'projects_rename',
231
+ 'account_get-context',
232
+ 'assets_list-creators',
233
+ 'assets_get-playback-metadata',
234
+ 'generations_get-continuation',
235
+ 'speech-previews_get-options',
236
+ 'speech-previews_create',
237
+ 'speech-previews_get',
238
+ 'dialogue_get-options',
239
+ 'dialogue_upload-source',
240
+ 'dialogue-transcriptions_create',
241
+ 'dialogue-transcriptions_get',
242
+ 'dialogue-previews_create',
243
+ 'dialogue-previews_get',
244
+ 'dialogue-videos_estimate',
245
+ 'dialogue-videos_create',
246
+ 'assets_upload',
247
+ 'lipsync_create',
248
+ 'lipsync_create-from-assets',
249
+ 'dubbing_create',
250
+ 'dubbing_get-options',
251
+ 'translation-previews_estimate',
252
+ 'translation-previews_create',
253
+ 'translation-previews_get',
254
+ 'audio-trims_create',
255
+ 'audio-trims_get',
256
+ 'voices_list',
257
+ 'generations_get',
258
+ 'models_list',
259
+ 'projects_list',
260
+ 'projects_get',
261
+ 'assets_list',
262
+ 'assets_count',
263
+ 'assets_get',
264
+ 'assets_update',
265
+ 'assets_delete',
266
+ 'assets_get-download-url',
267
+ 'assets_create-upload-url',
268
+ 'assets_create',
269
+ 'projects_create',
270
+ 'generations_list',
271
+ 'projects_list-generations',
272
+ 'generations_estimate-cost',
273
+ ]);
274
+ export function createToolDescriptorMeta(meta) {
275
+ const descriptorMeta = {
276
+ ...meta,
277
+ securitySchemes: meta?.securitySchemes ?? TOOL_SECURITY_SCHEMES,
12
278
  };
13
- const authHeaders = await resolveAuth(config, log);
14
- const httpClient = createHttpClient(config.baseUrl, authHeaders);
15
- log(`Fetching OpenAPI spec from ${config.baseUrl}...\n`);
16
- const spec = await fetchSpec(config.baseUrl);
17
- const operations = parseSpec(spec);
18
- log(`Discovered ${operations.length} API operations\n`);
19
- const tools = generateTools(operations, httpClient);
20
- const server = new McpServer({
21
- name: 'sync',
22
- version: '0.1.0',
23
- });
279
+ for (const key of ['openai/toolInvocation/invoking', 'openai/toolInvocation/invoked']) {
280
+ const text = descriptorMeta[key];
281
+ if (typeof text === 'string')
282
+ descriptorMeta[key] = text.toLowerCase();
283
+ }
284
+ return descriptorMeta;
285
+ }
286
+ // SDK clients validate structured errors too. Retained widgets require this
287
+ // envelope; success registration and projection remain independently strict.
288
+ const toolErrorOutputSchema = z.toJSONSchema(z.object({
289
+ error: z.object({
290
+ message: z.string(),
291
+ status: z.number().int().optional(),
292
+ code: z.string().optional(),
293
+ retryable: z.boolean().optional(),
294
+ retryAfterMs: z.number().finite().optional(),
295
+ generationId: z.string().optional(),
296
+ importJobId: z.uuid().optional(),
297
+ importStatus: z
298
+ .enum(['queued', 'downloading', 'processing', 'completed', 'failed'])
299
+ .optional(),
300
+ progress: z.number().min(0).max(100).optional(),
301
+ }),
302
+ }));
303
+ // The SDK wraps raw shapes on registration. Reuse immutable validators across
304
+ // sessions while keeping handlers, authentication and client profiles local.
305
+ const toolInputSchemas = new WeakMap();
306
+ function toolInputSchema(shape) {
307
+ let schema = toolInputSchemas.get(shape);
308
+ if (!schema) {
309
+ schema = z.object({
310
+ ...shape,
311
+ syncAnalytics: uiAnalyticsSchema
312
+ .optional()
313
+ .describe('Private UI instrumentation. Omit when calling from conversation.'),
314
+ });
315
+ toolInputSchemas.set(shape, schema);
316
+ }
317
+ return schema;
318
+ }
319
+ const toolSchemas = new WeakMap();
320
+ function toolSchema(shape) {
321
+ let schema = toolSchemas.get(shape);
322
+ if (!schema) {
323
+ schema = z.object(shape);
324
+ toolSchemas.set(shape, schema);
325
+ }
326
+ return schema;
327
+ }
328
+ export function registerTools(server, tools, authMetadataUrl, onToolError, decorateResult = (result) => result) {
329
+ const names = new Set(tools.map((tool) => tool.name));
24
330
  for (const tool of tools) {
25
- server.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
331
+ const config = {
332
+ title: tool.title?.toLowerCase(),
333
+ description: tool.description,
334
+ inputSchema: toolInputSchema(tool.inputSchema),
335
+ outputSchema: tool.outputSchema ? toolSchema(tool.outputSchema) : undefined,
336
+ annotations: tool.annotations,
337
+ _meta: createToolDescriptorMeta(tool.meta),
338
+ };
339
+ const run = async (args, ctx) => {
340
+ // Tools take the request's signal and _meta, not the SDK handler context.
341
+ const context = {
342
+ signal: ctx.mcpReq.signal,
343
+ _meta: ctx.mcpReq._meta,
344
+ progress: progressSender(ctx.mcpReq._meta?.progressToken, (notification) => ctx.mcpReq.notify(notification)),
345
+ };
26
346
  try {
27
- const result = await tool.handler((args ?? {}));
28
- return {
29
- content: [
30
- {
31
- type: 'text',
32
- text: JSON.stringify(result, null, 2),
33
- },
34
- ],
35
- };
347
+ if (tool.resultFormat === 'mcp') {
348
+ return decorateResult(await tool.handler((args ?? {}), context));
349
+ }
350
+ const result = await tool.handler((args ?? {}), context);
351
+ return decorateResult(createJsonToolResult(result, tool.outputSchema));
36
352
  }
37
353
  catch (error) {
38
- const message = error instanceof Error ? error.message : String(error);
354
+ onToolError?.(tool.name, error);
355
+ return decorateResult(createToolErrorResult(error, authMetadataUrl));
356
+ }
357
+ };
358
+ server.registerTool(tool.name, config, run);
359
+ // The previous name stays callable, unlisted, and says what the tool is called now.
360
+ // An old name that is now another tool's name (generations_estimate-cost) is that tool.
361
+ const legacy = oldNameAlias(tool.name);
362
+ if (legacy && !names.has(legacy))
363
+ server.registerTool(legacy, config, async (args, ctx) => {
364
+ const result = await run(args, ctx);
39
365
  return {
40
- content: [{ type: 'text', text: `Error: ${message}` }],
41
- isError: true,
366
+ ...result,
367
+ content: [
368
+ ...(result.content ?? []),
369
+ { type: 'text', text: renamedToolNotice(legacy, tool.name) },
370
+ ],
42
371
  };
372
+ });
373
+ }
374
+ }
375
+ // Agents resend a rejected request every few seconds unless told what changes the outcome.
376
+ const RESUBMIT_FAILS = 'Sending the same request again fails the same way. Explain the message to the user and change the request before trying again.';
377
+ const NEXT_STEP_BY_CODE = {
378
+ concurrency_limit_reached: 'A generation is still running for this account. Poll generations_get with its id, or list running ones with generations_list and status PROCESSING, until it finishes, then submit once. Submitting again before then fails the same way.',
379
+ generation_plan_duration_exceeded: 'This plan limits how long the output can be. Shorten the script or audio (audio-trims_create keeps the first part of saved audio), quote again, then create once.',
380
+ feature_not_enabled: 'This Sync account does not have the feature yet. Tell the user it needs to be enabled for their account; calling again fails the same way.',
381
+ stem_separation_source_not_hosted: 'Stem separation reads only files in Sync storage. Upload the file (assets_create-upload-url, PUT the bytes, then assets_create with the returned URL) and separate the new asset.',
382
+ };
383
+ function nextStep(error) {
384
+ const organization = organizationNextStep(error);
385
+ if (organization)
386
+ return organization;
387
+ if (error.code && NEXT_STEP_BY_CODE[error.code])
388
+ return NEXT_STEP_BY_CODE[error.code];
389
+ return [400, 402, 403, 422].includes(error.status) ? RESUBMIT_FAILS : undefined;
390
+ }
391
+ /** One stdout line per failed tool call, for the hosted server's logs. */
392
+ export function logToolError(profile) {
393
+ return (tool, error) => {
394
+ const api = error instanceof ApiRequestError ? error : undefined;
395
+ process.stdout.write(`${JSON.stringify({
396
+ timestamp: new Date().toISOString(),
397
+ event: 'mcp_tool_error',
398
+ tool,
399
+ profile: profile(),
400
+ status: api?.status,
401
+ code: api?.code,
402
+ message: (error instanceof Error ? error.message : String(error)).slice(0, 300),
403
+ })}\n`);
404
+ };
405
+ }
406
+ export function createToolErrorResult(error, authMetadataUrl) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ if (error instanceof ApiRequestError) {
409
+ const next = error.nextStep ?? nextStep(error);
410
+ const structuredContent = {
411
+ error: {
412
+ message,
413
+ status: error.status,
414
+ ...(error.code ? { code: error.code } : {}),
415
+ ...(next ? { nextStep: next } : {}),
416
+ ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
417
+ ...(error.generationId ? { generationId: error.generationId } : {}),
418
+ ...error.importDetails,
419
+ ...error.details,
420
+ },
421
+ };
422
+ return {
423
+ content: [
424
+ {
425
+ type: 'text',
426
+ text: JSON.stringify(structuredContent),
427
+ },
428
+ ],
429
+ structuredContent,
430
+ isError: true,
431
+ ...(error.status === 401 && authMetadataUrl
432
+ ? {
433
+ _meta: {
434
+ 'mcp/www_authenticate': [
435
+ `Bearer resource_metadata="${authMetadataUrl}", error="invalid_token", error_description="Reconnect your Sync account to continue"`,
436
+ ],
437
+ },
438
+ }
439
+ : {}),
440
+ };
441
+ }
442
+ const retryableError = error && typeof error === 'object'
443
+ ? error
444
+ : {};
445
+ if (typeof retryableError.code === 'string' &&
446
+ retryableError.retryable === true &&
447
+ typeof retryableError.retryAfterMs === 'number') {
448
+ const structuredContent = {
449
+ error: {
450
+ message,
451
+ code: retryableError.code,
452
+ retryable: true,
453
+ retryAfterMs: retryableError.retryAfterMs,
454
+ },
455
+ };
456
+ return {
457
+ content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
458
+ structuredContent,
459
+ isError: true,
460
+ };
461
+ }
462
+ return {
463
+ content: [{ type: 'text', text: `Error: ${message}` }],
464
+ isError: true,
465
+ };
466
+ }
467
+ export function createJsonToolResult(result, outputSchema) {
468
+ const resultForClient = outputSchema && isStructuredContent(result)
469
+ ? pickOutputSchemaFields(result, outputSchema)
470
+ : result;
471
+ const callToolResult = {
472
+ content: [
473
+ {
474
+ type: 'text',
475
+ text: JSON.stringify(resultForClient, null, 2),
476
+ },
477
+ ],
478
+ };
479
+ if (isStructuredContent(resultForClient)) {
480
+ callToolResult.structuredContent = resultForClient;
481
+ }
482
+ return callToolResult;
483
+ }
484
+ function pickOutputSchemaFields(result, outputSchema) {
485
+ const picked = {};
486
+ for (const key of Object.keys(outputSchema)) {
487
+ if (key in result) {
488
+ picked[key] = result[key];
489
+ }
490
+ }
491
+ return picked;
492
+ }
493
+ function isStructuredContent(result) {
494
+ return result !== null && typeof result === 'object' && !Array.isArray(result);
495
+ }
496
+ // Studio bridge tools need a viewId or draftId that only the ChatGPT widget publishes.
497
+ const STUDIO_BRIDGE_TOOLS = new Set([
498
+ 'studio-views_get',
499
+ 'studio-views_open-project',
500
+ 'studio-views_refresh-project',
501
+ 'studio-views_refresh-assets',
502
+ 'studio-views_show-generation',
503
+ 'studio-views_continue-generation',
504
+ 'studio-drafts_get',
505
+ 'studio-drafts_update',
506
+ ]);
507
+ /** Tools that only work inside the ChatGPT app UI, including widget-only (non-model) tools. */
508
+ function isAppOnlyTool(tool) {
509
+ const visibility = asRecord(tool.meta?.ui).visibility;
510
+ return (APP_UI_TOOLS.has(tool.name) ||
511
+ STUDIO_BRIDGE_TOOLS.has(tool.name) ||
512
+ (Array.isArray(visibility) && !visibility.includes('model')));
513
+ }
514
+ function isToolVisible(tool, profile) {
515
+ if (isAppOnlyTool(tool))
516
+ return profile.supportsAppUi;
517
+ if (tool.name === 'lipsync_create-from-assets')
518
+ return profile.supportsAppUi;
519
+ // Kept out of the reviewed ChatGPT app listing; its embedded Studio has no timeline.
520
+ if (tool.name === 'lipsync_create-timeline')
521
+ return profile.name !== 'chatgpt';
522
+ // Agents report friction; the reviewed ChatGPT app listing keeps its tool set.
523
+ if (tool.name === 'feedback_send')
524
+ return profile.name !== 'chatgpt';
525
+ // Not in the reviewed ChatGPT app listing yet.
526
+ if (BATCH_TOOLS.includes(tool.name))
527
+ return profile.name !== 'chatgpt';
528
+ // Not in the reviewed ChatGPT app listing yet; other profiles list them per account.
529
+ if (isFeatureGatedTool(tool.name))
530
+ return profile.name !== 'chatgpt';
531
+ if (['assets_upload', 'assets_open-upload-widget'].includes(tool.name))
532
+ return profile.supportsUploads;
533
+ return true;
534
+ }
535
+ export function selectHostedHttpTools(tools, profile = resolveClientProfile()) {
536
+ return tools
537
+ .filter((tool) => HOSTED_HTTP_TOOL_ALLOWLIST.has(tool.name))
538
+ .filter((tool) => isToolVisible(tool, profile))
539
+ .map((tool) => presentTool(tool, profile));
540
+ }
541
+ function exposeToolToWidget(tool) {
542
+ const meta = tool.meta ?? {};
543
+ const ui = asRecord(meta.ui);
544
+ return {
545
+ ...tool,
546
+ meta: {
547
+ ...meta,
548
+ ui: {
549
+ ...ui,
550
+ visibility: ui.visibility ?? ['model', 'app'],
551
+ },
552
+ 'openai/widgetAccessible': true,
553
+ },
554
+ };
555
+ }
556
+ function asRecord(value) {
557
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
558
+ return value;
559
+ }
560
+ return {};
561
+ }
562
+ /** Profile-specific descriptors and presentation. Authentication remains request-scoped. */
563
+ function presentTool(tool, profile) {
564
+ if (profile.supportsUploads)
565
+ return WIDGET_CALLABLE_HOSTED_TOOLS.has(tool.name) ? exposeToolToWidget(tool) : tool;
566
+ const inputSchema = { ...tool.inputSchema };
567
+ let description = tool.description;
568
+ if (tool.name === 'lipsync_create') {
569
+ for (const key of ['video', 'image', 'audio'])
570
+ delete inputSchema[key];
571
+ description =
572
+ 'Create one lipsync generation from exactly one image/video URL or Sync asset ID and one audio URL/asset ID or script plus a voiceId returned by voices_list. Defaults to sync-3; new work creates a NEW project, even with a duplicate projectName. Supply a fresh projectCreationKey for keyed creation; preserve it on retries. Use projectId only for an explicit user selection and supply projectIntent=explicit. Poll the returned id with generations_get.';
573
+ for (const key of ['videoAssetId', 'imageAssetId', 'audioAssetId']) {
574
+ inputSchema[key] = z
575
+ .string()
576
+ .optional()
577
+ .describe('Existing Sync asset ID in the same organization.');
578
+ }
579
+ inputSchema.videoUrl = z
580
+ .string()
581
+ .optional()
582
+ .describe('Public or Sync-hosted video URL. Supply exactly one visual input.');
583
+ }
584
+ const meta = profile.supportsAppUi
585
+ ? tool.meta
586
+ : Object.fromEntries(Object.entries(tool.meta ?? {}).filter(([key]) => !key.startsWith('openai/') && key !== 'ui'));
587
+ const presented = { ...tool, inputSchema, description, meta };
588
+ return profile.supportsAppUi && WIDGET_CALLABLE_HOSTED_TOOLS.has(tool.name)
589
+ ? exposeToolToWidget(presented)
590
+ : presented;
591
+ }
592
+ // Schemas and stateless handlers belong to the factory, not each HTTP session.
593
+ // Authentication is resolved per request by createHttpClient; presentation stays local.
594
+ function createTools(config, operations, httpClient, getProfile, chatgptApp) {
595
+ // stdio authenticates with SYNC_API_KEY when set; otherwise, like hosted, as a user.
596
+ const credential = config.transport === 'stdio' && config.apiKey ? 'api_key' : 'user';
597
+ // These operations require the durable wrapper's admission key and public projection.
598
+ const generatedOperations = operations.filter((operation) => operation.operationId !== 'analytics_captureChatgptEvent' &&
599
+ !feedbackOperation(operation) &&
600
+ translationPreviewOperations([operation]).length === 0 &&
601
+ !operation.path.startsWith('/v2/audio-trims') &&
602
+ !isBatchOperation(operation) &&
603
+ operation.studioDraftVersion !== 1 &&
604
+ operation.studioViewVersion !== 1 &&
605
+ !generationCostsOperation(operation) &&
606
+ !nameTakenByRenamedTool(derivedToolName(operation.operationId)));
607
+ const exposedOperations = config.transport === 'http'
608
+ ? generatedOperations.filter((operation) => HOSTED_HTTP_TOOL_ALLOWLIST.has(operationIdToToolName(operation.operationId)))
609
+ : generatedOperations;
610
+ const generatedTools = generateTools(exposedOperations, httpClient).map((tool, index) => adaptGeneratedTool(tool, exposedOperations[index], httpClient));
611
+ const analyticsAvailable = operations.some((operation) => operation.operationId === 'analytics_captureChatgptEvent');
612
+ const allTools = [
613
+ ...(analyticsAvailable ? [createAnalyticsTool(httpClient)] : []),
614
+ ...(chatgptApp
615
+ ? [
616
+ createOpenSyncAppTool(chatgptApp, generatedTools.find((tool) => tool.name === 'projects_list')),
617
+ ]
618
+ : []),
619
+ createUploadWidgetTool(),
620
+ createAccountContextTool(httpClient, supportsStudioDrafts(operations), supportsStudioViews(operations), supportsAudioTrimming(operations), analyticsAvailable, credential),
621
+ ...createOrganizationTools(httpClient, credential),
622
+ ...createStudioViewTools(operations, httpClient),
623
+ ...createStudioDraftTools(operations, httpClient),
624
+ ...createAccountManagementTools(httpClient),
625
+ ...createGenerationCostsTools(operations, httpClient),
626
+ createAssetCreatorsTool(httpClient),
627
+ createPlaybackMetadataTool(httpClient),
628
+ createGenerationContinuationTool(httpClient),
629
+ ...createAppTools(httpClient, undefined, getProfile),
630
+ createTimelineLipsyncTool(httpClient, getProfile, {
631
+ maxClips: timelineClipLimit(operations),
632
+ keptClips: timelineKeepsClips(operations),
633
+ }),
634
+ ...createProjectHistoryTools(operations, httpClient),
635
+ ...createTranslationTools(operations, httpClient),
636
+ ...createTranslationPreviewTools(operations, httpClient),
637
+ ...createAudioTrimTools(operations, httpClient),
638
+ ...createBatchTools(operations, httpClient, config.baseUrl),
639
+ ...createDialogueTools(operations, httpClient),
640
+ ...createSpeechTools(operations, httpClient),
641
+ ...createFeedbackTools(operations, httpClient, PACKAGE_VERSION),
642
+ ...createAssetAnalysisTools(httpClient),
643
+ ...generatedTools.map((tool) => config.chatgptApp?.uploadStorageOrigin
644
+ ? relayUploadTool(tool, config.chatgptApp.domain, config.chatgptApp.uploadStorageOrigin)
645
+ : tool),
646
+ ];
647
+ const adapted = allTools
648
+ .map(withProjectIntent)
649
+ .map((tool) => withOrganizationChoice(tool, httpClient, credential))
650
+ .map(withGenerationStatusCard)
651
+ .map(withGenerationOutputAliases)
652
+ .map((tool) => (config.spendConfirmation === false ? withoutSpendConfirmation(tool) : tool));
653
+ // Built from every tool, so jobs_wait reaches a kind whose own tools a toolset leaves out.
654
+ const tools = [...adapted, ...createJobTools(adapted)];
655
+ return config.transport === 'http'
656
+ ? tools.filter((tool) => HOSTED_HTTP_TOOL_ALLOWLIST.has(tool.name))
657
+ : tools.filter((tool) => toolsetIncludes(config.toolsets, tool.name, HOSTED_HTTP_TOOL_ALLOWLIST));
658
+ }
659
+ function createProfiledServer(config, operations, authHeaders = {}, chatgptApp, previousApps = [], sharedCatalog, httpClientName,
660
+ // An initialize request resolves the profile through the handshake, but its
661
+ // response carries the instructions, so they follow the announced client.
662
+ handshakeClientName, updateCheck) {
663
+ let profile;
664
+ let clientName = httpClientName;
665
+ const getProfile = () => profile ?? resolveClientProfile();
666
+ const httpClient = createHttpClient(config.baseUrl, authHeaders, config.transport === 'stdio' ? () => clientName : undefined);
667
+ const tools = sharedCatalog?.tools ?? createTools(config, operations, httpClient, getProfile, chatgptApp);
668
+ // Hosted sessions supply theirs per request (HttpSession); stdio keeps one for the process.
669
+ const ownOrganizationSession = new OrganizationSession();
670
+ const analytics = toolAnalyticsSupport(operations);
671
+ const server = new McpServer({
672
+ name: 'sync',
673
+ version: PACKAGE_VERSION,
674
+ description: serverDescription(config.spendConfirmation),
675
+ }, {
676
+ instructions: serverInstructions(resolveClientProfile(httpClientName ?? handshakeClientName), config.spendConfirmation, operations.some(feedbackOperation),
677
+ // The hosted server deploys on every merge, so only local installs report updates.
678
+ config.transport === 'stdio' ? updateCheck?.available() : undefined, config.transport === 'stdio' ? toolsetInstruction(config.toolsets) : ''),
679
+ });
680
+ // Keep the SDK's input/output validation and execution. Hidden upload tools remain
681
+ // callable only to return an actionable error; they cannot perform any upload.
682
+ registerTools(server, tools.map((tool) => ({
683
+ ...tool,
684
+ handler: async (...parameters) => {
685
+ const [args] = parameters;
686
+ if (!profile)
687
+ throw new Error('Complete MCP initialization before calling tools.');
688
+ if (isAppOnlyTool(tool) && !profile.supportsAppUi)
689
+ throw new Error('Open the Sync app in ChatGPT, or use the Sync tools in this client.');
690
+ if (!profile.supportsUploads &&
691
+ (['assets_upload', 'assets_open-upload-widget'].includes(tool.name) ||
692
+ (tool.name === 'lipsync_create' &&
693
+ ['video', 'image', 'audio'].some((key) => args[key] !== undefined)))) {
694
+ throw new Error(UNSUPPORTED_UPLOAD_MESSAGE);
43
695
  }
696
+ const activeProfile = profile;
697
+ const organizationSession = currentOrganizationSession() ?? ownOrganizationSession;
698
+ const execute = () => trackToolExecution(
699
+ // The analytics collector still names tools as they were before the rename.
700
+ previousToolName(tool.name) ?? tool.name, args, parameters[1]?._meta, activeProfile.name, httpClient, (businessArgs) => tool.handler(businessArgs, {
701
+ ...parameters[1],
702
+ clientProfile: activeProfile,
703
+ clientName,
704
+ // Only stdio holds a connection that can carry an unrelated notification.
705
+ ...(config.transport === 'stdio'
706
+ ? {
707
+ toolListChanged: () => void server.server.sendToolListChanged().catch(() => { }),
708
+ }
709
+ : {}),
710
+ }), analytics);
711
+ // Each call keeps the organization it started in, even if the session switches.
712
+ const organization = organizationForCall(tool, args, organizationSession);
713
+ return runInOrganizationSession(organizationSession, () => runInOrganization(organization, execute));
714
+ },
715
+ })), config.transport === 'http' ? resourceMetadataUrl(config.port) : undefined,
716
+ // stdout is the protocol channel over stdio.
717
+ config.transport === 'http' ? logToolError(() => getProfile().name) : undefined, config.transport === 'stdio' && updateCheck ? withUpdateNotice(updateCheck) : undefined);
718
+ const resources = [];
719
+ resources.push({
720
+ name: 'sync-generation-status',
721
+ uri: GENERATION_STATUS_URI,
722
+ ...GENERATION_STATUS_METADATA,
723
+ });
724
+ server.registerResource('sync-generation-status', GENERATION_STATUS_URI, GENERATION_STATUS_METADATA, async () => {
725
+ if (!getProfile().supportsAppUi)
726
+ throw new Error('This interface requires an app-compatible client.');
727
+ return {
728
+ contents: [
729
+ {
730
+ uri: GENERATION_STATUS_URI,
731
+ mimeType: GENERATION_STATUS_METADATA.mimeType,
732
+ text: GENERATION_STATUS_HTML,
733
+ _meta: GENERATION_STATUS_METADATA._meta,
734
+ },
735
+ ],
736
+ };
737
+ });
738
+ registerUploadWidgetResource({
739
+ registerResource: (name, uri, metadata, read) => {
740
+ resources.push({ name, uri, ...metadata });
741
+ return server.registerResource(name, uri, metadata, async (url, ctx) => {
742
+ if (!getProfile().supportsUploads)
743
+ throw new Error(UNSUPPORTED_UPLOAD_MESSAGE);
744
+ return read(url, ctx);
745
+ });
746
+ },
747
+ });
748
+ for (const [index, app] of (chatgptApp ? [chatgptApp, ...previousApps] : []).entries()) {
749
+ if (index === 0)
750
+ resources.push({ name: 'sync-app', uri: app.uri, ...app.metadata });
751
+ server.registerResource(index === 0 ? 'sync-app' : `sync-app-previous-${index}`, app.uri, app.metadata, async () => {
752
+ if (!getProfile().supportsAppUi)
753
+ throw new Error('This interface requires an app-compatible client.');
754
+ process.stderr.write(`${JSON.stringify({ event: 'app_resource_served', timestamp: new Date().toISOString(), clientProfile: getProfile().name, resource: app.uri, bytes: Buffer.byteLength(app.html) })}\n`);
755
+ return {
756
+ contents: [
757
+ {
758
+ uri: app.uri,
759
+ mimeType: app.metadata.mimeType,
760
+ text: app.html,
761
+ _meta: app.metadata._meta,
762
+ },
763
+ ],
764
+ };
44
765
  });
45
766
  }
46
- log(`Registered ${tools.length} MCP tools\n`);
767
+ let descriptors = [];
768
+ server.server.setRequestHandler('tools/list', async () => {
769
+ if (!profile)
770
+ throw new Error('Complete MCP initialization before listing tools.');
771
+ // Feature-gated tools follow the session's active organization (organizations_select).
772
+ const session = currentOrganizationSession() ?? ownOrganizationSession;
773
+ return { tools: await listedForAccount(descriptors, session, httpClient) };
774
+ });
775
+ server.server.setRequestHandler('resources/list', () => ({
776
+ resources: resources.filter((resource) => resource.name === 'sync-app' || resource.name === 'sync-generation-status'
777
+ ? getProfile().supportsAppUi
778
+ : getProfile().supportsUploads),
779
+ }));
780
+ const onInitialized = server.server.oninitialized;
781
+ server.server.oninitialized = () => {
782
+ if (!profile) {
783
+ clientName ??= server.server.getClientVersion()?.name;
784
+ const initializedProfile = resolveClientProfile(clientName);
785
+ profile = initializedProfile;
786
+ const cached = sharedCatalog?.descriptors.get(initializedProfile.name);
787
+ if (cached) {
788
+ descriptors = cached;
789
+ onInitialized?.();
790
+ return;
791
+ }
792
+ const visible = config.transport === 'http'
793
+ ? selectHostedHttpTools(tools, initializedProfile)
794
+ : tools
795
+ .filter((tool) => isToolVisible(tool, initializedProfile))
796
+ .map((tool) => presentTool(tool, initializedProfile));
797
+ descriptors = visible.map((tool) => ({
798
+ name: tool.name,
799
+ title: tool.title?.toLowerCase(),
800
+ description: tool.description,
801
+ // Only the ChatGPT app UI sends syncAnalytics; other clients never see the field.
802
+ inputSchema: z.toJSONSchema(initializedProfile.supportsAppUi
803
+ ? toolInputSchema(tool.inputSchema)
804
+ : toolSchema(tool.inputSchema)),
805
+ ...(tool.outputSchema
806
+ ? {
807
+ outputSchema: {
808
+ type: 'object',
809
+ anyOf: [z.toJSONSchema(z.object(tool.outputSchema)), toolErrorOutputSchema],
810
+ },
811
+ }
812
+ : {}),
813
+ annotations: tool.annotations,
814
+ _meta: createToolDescriptorMeta(tool.meta),
815
+ securitySchemes: tool.meta?.securitySchemes ?? TOOL_SECURITY_SCHEMES,
816
+ }));
817
+ if (initializedProfile.supportsAppUi)
818
+ descriptors = withLegacyAppDescriptors(descriptors);
819
+ sharedCatalog?.descriptors.set(initializedProfile.name, descriptors);
820
+ }
821
+ onInitialized?.();
822
+ };
823
+ if (httpClientName !== undefined)
824
+ server.server.oninitialized();
47
825
  return server;
48
826
  }
827
+ function rememberedElsewhere(remembered, session) {
828
+ const id = remembered?.id ?? session.defaultId();
829
+ const where = remembered
830
+ ? `organization ${remembered.name ?? remembered.id}`
831
+ : 'the default organization of this connection';
832
+ return new Error(`This request uses an id created in ${where}, but this session now runs in ${session.selected?.name ?? 'another organization'}. ${id ? `Pass organizationId "${id}" to work in that organization` : 'Switch back with organizations_select'}, or use media and projects from the current one.`);
833
+ }
834
+ // Reads that follow ids a one-call organizationId created; jobs_* only read jobs too.
835
+ function followsRememberedIds(tool) {
836
+ return tool.annotations?.readOnlyHint === true || ['jobs_get', 'jobs_wait'].includes(tool.name);
837
+ }
838
+ /**
839
+ * The organization a call runs in: the session's selection, or, for a read of an id that a
840
+ * one-call organizationId created, that organization. A write naming such an id in another
841
+ * organization than the session's is refused unless it names the organization itself.
842
+ */
843
+ function organizationForCall(tool, args, session) {
844
+ const remembered = session.organizationFor(args);
845
+ if (remembered === undefined || session.isCurrent(remembered))
846
+ return session.selected;
847
+ // null: the credential's own organization, which a call reaches by sending no header.
848
+ if (followsRememberedIds(tool))
849
+ return remembered ?? undefined;
850
+ if (args.organizationId === undefined)
851
+ throw rememberedElsewhere(remembered, session);
852
+ return session.selected;
853
+ }
854
+ /**
855
+ * Adds the update notice to the first tool result after the check finds a newer version,
856
+ * once per session, including when it finished after the instructions were sent. It is a
857
+ * trailing text block, so structuredContent and the leading JSON text stay unchanged.
858
+ */
859
+ function withUpdateNotice(updateCheck) {
860
+ let delivered = false;
861
+ return (result) => {
862
+ const update = delivered ? undefined : updateCheck.available();
863
+ if (!update)
864
+ return result;
865
+ delivered = true;
866
+ return {
867
+ ...result,
868
+ content: [...(result.content ?? []), { type: 'text', text: updateToolNotice(update) }],
869
+ };
870
+ };
871
+ }
872
+ export async function createSyncMcpServer(config, options = {}) {
873
+ const log = (message) => {
874
+ process.stderr.write(message);
875
+ };
876
+ const authHeaders = config.transport === 'http' ? {} : await resolveAuth(config, log);
877
+ const operations = parseSpec(await fetchSpec(config.baseUrl));
878
+ const [chatgptApp, ...previousApps] = await loadChatgptAppReleases(config.chatgptApp);
879
+ return createProfiledServer(config, operations, authHeaders, chatgptApp, previousApps, undefined, undefined, undefined, options.updateCheck);
880
+ }
881
+ export async function createMcpServerFactory(config) {
882
+ const [chatgptApp, ...previousApps] = await loadChatgptAppReleases(config.chatgptApp);
883
+ // Filter and build schemas once. Session profiles and SDK transports stay local.
884
+ const operations = parseSpec(await fetchSpec(config.baseUrl)).filter((operation) => operation.operationId === 'analytics_captureChatgptEvent' ||
885
+ feedbackOperation(operation) ||
886
+ // Read for its published segment limit; raw generation stays unhosted.
887
+ (operation.method === 'post' && operation.path === '/v2/generate') ||
888
+ HOSTED_HTTP_TOOL_ALLOWLIST.has(operationIdToToolName(operation.operationId)) ||
889
+ generationCostsOperation(operation) ||
890
+ translationOperation([operation]) !== undefined ||
891
+ translationPreviewOperations([operation]).length > 0 ||
892
+ operation.path === '/v2/audio-trims' ||
893
+ operation.path === '/v2/audio-trims/{id}' ||
894
+ isBatchOperation(operation) ||
895
+ dialogueOperations([operation]).length > 0 ||
896
+ speechOperations([operation]).length > 0 ||
897
+ operation.studioDraftVersion === 1 ||
898
+ operation.studioViewVersion === 1);
899
+ const tools = createTools(config, operations, createHttpClient(config.baseUrl), resolveClientProfile, chatgptApp);
900
+ // Four presentation profiles at most; never key this by untrusted client names.
901
+ const sharedCatalog = {
902
+ tools,
903
+ descriptors: new Map(),
904
+ };
905
+ return {
906
+ // Number registered across hosted profiles, not each client's visible catalog.
907
+ toolCount: new Set(tools.map((tool) => tool.name)).size,
908
+ createServer: (clientName, handshakeClientName) => createProfiledServer(config, operations, {}, chatgptApp, previousApps, sharedCatalog, clientName, handshakeClientName),
909
+ };
910
+ }
49
911
  async function resolveAuth(config, log) {
50
912
  if (config.apiKey) {
51
913
  log('Using API key authentication\n');
@@ -56,7 +918,6 @@ async function resolveAuth(config, log) {
56
918
  log('Using cached device auth token\n');
57
919
  return {
58
920
  Authorization: `Bearer ${cachedToken}`,
59
- 'x-sync-source': 'mcp',
60
921
  };
61
922
  }
62
923
  log('No API key or cached token found. Starting device auth...\n');