@contenthero/mcp 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +1 -1
- package/dist/format.d.ts +150 -6
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +217 -12
- package/dist/format.js.map +1 -1
- package/dist/groups.js +1 -1
- package/dist/groups.js.map +1 -1
- package/dist/server.d.ts +68 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +497 -57
- package/dist/server.js.map +1 -1
- package/dist/widget/generation.html +516 -0
- package/dist/widget-uri.d.ts +12 -0
- package/dist/widget-uri.d.ts.map +1 -0
- package/dist/widget-uri.js +12 -0
- package/dist/widget-uri.js.map +1 -0
- package/package.json +13 -4
package/dist/client.js
CHANGED
|
@@ -33,7 +33,7 @@ export function getClient() {
|
|
|
33
33
|
}
|
|
34
34
|
// Announced once, on stderr so it never corrupts the stdio protocol stream. Which ACCOUNT an MCP server is
|
|
35
35
|
// acting as is the single most consequential thing about it and was previously invisible: the only way to
|
|
36
|
-
// find out was to call get_balance and
|
|
36
|
+
// find out was to call get_balance and recognize the number.
|
|
37
37
|
console.error(`[contenthero-mcp] authenticated from ${keySource} (config dir: ${configDir()})`);
|
|
38
38
|
cached = new ContentHero({
|
|
39
39
|
apiKey,
|
package/dist/format.d.ts
CHANGED
|
@@ -6,8 +6,121 @@
|
|
|
6
6
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import type { Avatar, AvatarSummary, Balance, BrandKit, BrandKitSummary, BrandKitSectionRecord, BrandKnowledgeItem, BrandKnowledgeDetail, BrandKnowledgeListResult, BrandKnowledgeMatch, ConnectedAccount, CostEstimate, CreateAvatarResult, Element, Generation, GenerateResult, EditAudioResult, MediaItem, MediaSummary, SearchMediaResult, Folder, DerivedFolder, FolderItem, MediaBatchResult, CreateMediaUploadResult, ImportedMedia, UploadedMedia, ModelInfo, PlatformSummary, PlatformSchema, Stage, Space, CardAsset, Post, CardDetail, CardListResult, StageListResult, CardSummary, Tag, PublishResult, TrackedAccount, ContentDetail, ContentListResult, AccountDetail, Transcription, Voice, VoiceSummary, ApplyEditorOpsResult, ProjectSummary, ProjectDetail, LiveContextResult, LayerTypeCatalog, TimelineTypeCatalog, TranscriptResult, ExportJob, ExportFormatCatalog, ExtractionOutcome } from '@contenthero/sdk';
|
|
8
8
|
export declare function text(body: string, isError?: boolean): CallToolResult;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* An attachment for a finished generation: the bytes to inline, or a link for the host to render.
|
|
11
|
+
*
|
|
12
|
+
* ⭐⭐⭐ **THE SHAPE FOLLOWS THE MEDIUM, NOT OUR PREFERENCE.** MCP's `ContentBlock` union is
|
|
13
|
+
* `text | image | audio | resource_link | resource`. Images and audio have first-class blocks and are small
|
|
14
|
+
* enough to carry as bytes, so they are EMBEDDED: that is what makes them render in the chat and what makes
|
|
15
|
+
* them outlive any URL. Video has no block of its own and would be megabytes of base64 inside a transcript,
|
|
16
|
+
* so it travels as a `resource_link`, which is exactly what a working implementation does (verified against
|
|
17
|
+
* the Higgsfield MCP, whose `job_display` returns `[Resource link: ....mp4]`).
|
|
18
|
+
*/
|
|
19
|
+
export type GeneratedAttachment = {
|
|
20
|
+
kind: 'bytes';
|
|
21
|
+
type: 'image' | 'audio';
|
|
22
|
+
data: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'link';
|
|
26
|
+
uri: string;
|
|
27
|
+
mimeType: string;
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A finished generation: the asset URLs and placement outcome, plus the asset ITSELF.
|
|
32
|
+
*
|
|
33
|
+
* ## Why this used to be text only
|
|
34
|
+
*
|
|
35
|
+
* 🚨 **AN AGENT THAT GENERATES AN IMAGE COULD NOT SEE IT.** This returned a header, an outputId and a
|
|
36
|
+
* numbered list of urls, so the model had metadata and nothing else. Measured 2026-09-19 in a real ChatGPT
|
|
37
|
+
* and Claude session: Claude said "I can't see the image myself, only the metadata", and the link it
|
|
38
|
+
* surfaced was dead on arrival because ChatGPT had appended `utm_source=chatgpt.com` to a presigned URL and
|
|
39
|
+
* broken its signature.
|
|
40
|
+
*
|
|
41
|
+
* ⭐ The machinery already existed and was wired to the wrong tools: `mediaBatchResult` and
|
|
42
|
+
* `liveContextResult` have pushed image blocks for a while. Generation, the surface where a user most wants
|
|
43
|
+
* to SEE the result, was the one that did not.
|
|
44
|
+
*
|
|
45
|
+
* ## ⛔ THE AGENT CANNOT SEE WHAT IT MADE FROM THIS RESULT, AND THAT IS DELIBERATE
|
|
46
|
+
*
|
|
47
|
+
* An `image` block feeds the MODEL's vision; a `resource_link` gives the HOST something to render for the
|
|
48
|
+
* human. So the user sees every variation inline, and the model has a name and a url.
|
|
49
|
+
*
|
|
50
|
+
* ⭐ **`get_media` IS HOW A MODEL ACTUALLY LOOKS AT SOMETHING.** It embeds bytes as image blocks for exactly
|
|
51
|
+
* that purpose, so an agent that needs to judge a result (is the hand wrong, is the text legible, which of
|
|
52
|
+
* these four is best) calls it with the outputId. Embedding bytes HERE instead would spend the user's
|
|
53
|
+
* context on every generation to answer a question they may never ask, and a four-image batch would pay
|
|
54
|
+
* that cost four times over, repeated in every later turn.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ ATTACHMENTS ARE BUILT BY THE CALLER, not here. This module stays pure, the same split
|
|
57
|
+
* `mediaBatchResult` already uses: the handler decides, the formatter assembles.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* What the generation WIDGET reads.
|
|
61
|
+
*
|
|
62
|
+
* ⭐⭐ `structuredContent` IS THE WIDGET'S ONLY INPUT. It is a separate channel from `content`: the blocks
|
|
63
|
+
* feed the model and any host without app support, this feeds the UI. Both are emitted, so nothing regresses
|
|
64
|
+
* where widgets are unsupported and nothing is duplicated where they are.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ URLS ONLY, NEVER BYTES. The widget runs in the host's frame and fetches media itself, and our
|
|
67
|
+
* capability urls carry their token in the QUERY STRING, so `<video src>` loads one directly with no header
|
|
68
|
+
* to set. Embedding base64 here would pay the context cost twice over.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Where one output lives in the product.
|
|
72
|
+
*
|
|
73
|
+
* ## ⭐⭐⭐ COMPUTED HERE, NOT IN THE WIDGET, BECAUSE HERE THE INDEX IS ALREADY A NUMBER
|
|
74
|
+
*
|
|
75
|
+
* The widget's first version parsed the slot back out of the output's NAME (`<id>-3` means slot 2), which
|
|
76
|
+
* is a derivation of something this function has in its hand. Parsing a number out of a string we formatted
|
|
77
|
+
* two lines earlier is how an off-by-one gets in, and the symptom would be "Open shows the wrong picture",
|
|
78
|
+
* which reads as a broken link rather than an index bug.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **ONE-BASED IN THE NAME, ZERO-BASED IN THE URL.** `<id>-3` is what a person reads as "variation 3",
|
|
81
|
+
* and `variation=2` is the studio's `imageIndex`, which is slot space. Both conventions are correct in
|
|
82
|
+
* their own place and the conversion belongs at exactly one boundary, which is this one.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ A single-output generation gets NO `variation`. There is no variation to name, and passing 0 would
|
|
85
|
+
* imply there was a choice.
|
|
86
|
+
*/
|
|
87
|
+
/** Where a deep link points when no client base url was threaded through. */
|
|
88
|
+
export declare const DEFAULT_APP_URL = "https://app.contenthero.ai";
|
|
89
|
+
export declare function studioUrlFor(baseUrl: string, outputId: string, index: number, total: number): string;
|
|
90
|
+
export declare function generationWidgetData(gen: Generation, posterUrls?: readonly (string | null)[], baseUrl?: string): {
|
|
91
|
+
outputId: string;
|
|
92
|
+
contentType: "image" | "audio" | "video";
|
|
93
|
+
modelId: string;
|
|
94
|
+
/**
|
|
95
|
+
* ⛔⛔ **NULL WHEN THERE IS NOTHING TO NAME, AND THE WIDGET MUST RENDER NO CHIP.**
|
|
96
|
+
*
|
|
97
|
+
* This used to be `displayName ?? gen.modelId`, fed by a catalog fetch in the server with a `catch`
|
|
98
|
+
* that returned the id. The id reads like a label, so a failed fetch showed as a chip flickering
|
|
99
|
+
* between kebab case and title case rather than as a failure. The name now arrives on the row.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ It is not always this generation's own model: an upload names itself, and a look names the model
|
|
102
|
+
* that produced the image it was assembled from.
|
|
103
|
+
*/
|
|
104
|
+
modelName: string | null;
|
|
105
|
+
/** Hex brand accent from the registry, or null. The widget colors its chip with it. */
|
|
106
|
+
modelBrandColor: string | null;
|
|
107
|
+
/** Brand family key (e.g. `"openai"`) for choosing a glyph. NOT the model id. */
|
|
108
|
+
modelIconKey: string | null;
|
|
109
|
+
/** `"W:H"`, or null for audio. Drives the column count and the tile's own shape. */
|
|
110
|
+
displayAspect: string | null;
|
|
111
|
+
/** Verbatim. Some prompts are JSON-shaped because the person authored one; that object IS the prompt. */
|
|
112
|
+
prompt: string | null;
|
|
113
|
+
outputs: {
|
|
114
|
+
url: string;
|
|
115
|
+
posterUrl: string | null;
|
|
116
|
+
name: string;
|
|
117
|
+
/** Where the Open button goes: this asset, in the studio, with its siblings and every action. */
|
|
118
|
+
studioUrl: string;
|
|
119
|
+
}[];
|
|
120
|
+
};
|
|
121
|
+
export declare function completedResult(gen: Generation, attachments?: GeneratedAttachment[], posterUrls?: readonly (string | null)[],
|
|
122
|
+
/** The server this client talks to, so Open deep-links to it rather than always to production. */
|
|
123
|
+
baseUrl?: string): CallToolResult;
|
|
11
124
|
/** Suggested seconds to wait before re-polling a job, by content type. */
|
|
12
125
|
export declare function pollAfterSecondsFor(contentType: string): number;
|
|
13
126
|
/**
|
|
@@ -19,8 +132,39 @@ export declare function pollAfterSecondsFor(contentType: string): number;
|
|
|
19
132
|
* is what drifted, so these messages now print the call instead, and every site shares this one function.
|
|
20
133
|
*/
|
|
21
134
|
export declare function getStatusCall(outputIds: readonly string[]): string;
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
135
|
+
/** What a still-running generation already knows about the shape of its own result. */
|
|
136
|
+
export interface PendingShape {
|
|
137
|
+
contentType: 'image' | 'video' | 'audio';
|
|
138
|
+
modelId: string;
|
|
139
|
+
/** `"W:H"` as requested. Absent for audio and for `auto`/`adaptive`. */
|
|
140
|
+
displayAspect?: string | null;
|
|
141
|
+
/** How many outputs were asked for, so the widget draws that many placeholders. */
|
|
142
|
+
expected?: number;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A slow job that did not finish within the smart-wait window.
|
|
146
|
+
*
|
|
147
|
+
* ## ⭐⭐⭐ THIS IS WHERE THE SKELETONS COME FROM, AND WHY IT IS THE ONLY PLACE THEY COULD
|
|
148
|
+
*
|
|
149
|
+
* A generation returns one of two ways: it finished inside the smart wait, in which case there is nothing
|
|
150
|
+
* to show a spinner for, or it did not, and until now that produced ONE SENTENCE of prose asking the agent
|
|
151
|
+
* to poll. Every video takes that path. So the person who waited longest got the least: a paragraph, while
|
|
152
|
+
* the same job in the studio shows placeholder cards filling in.
|
|
153
|
+
*
|
|
154
|
+
* ⛔ **THE FIX IS NOT TO MAKE `generate_*` RETURN EARLY.** That would hand every host the pending path,
|
|
155
|
+
* including hosts with no MCP Apps support, which would lose the inline image they get today. The pending
|
|
156
|
+
* path already exists and already reaches exactly the people who are waiting.
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ **THE TEXT STAYS, WORD FOR WORD.** It is what a host without app support renders, and it is what the
|
|
159
|
+
* AGENT reads to know it must poll. The widget is added ALONGSIDE it, not instead of it: an agent that
|
|
160
|
+
* stopped polling because the prose was replaced by a payload it cannot see would leave the generation
|
|
161
|
+
* unclaimed.
|
|
162
|
+
*
|
|
163
|
+
* ⚠️ No model NAME here, and that is deliberate. Resolving one would mean a second network call from
|
|
164
|
+
* inside a `catch`, which is the exact shape that produced a chip flickering between kebab case and title
|
|
165
|
+
* case. The widget polls `get_generation_status`, and the name arrives with the first response.
|
|
166
|
+
*/
|
|
167
|
+
export declare function pendingResult(outputId: string, pollAfterSeconds?: number, shape?: PendingShape): CallToolResult;
|
|
24
168
|
/** Synchronous audio result (already complete on submit). */
|
|
25
169
|
export declare function audioResult(result: GenerateResult | EditAudioResult): CallToolResult;
|
|
26
170
|
/**
|
|
@@ -46,9 +190,9 @@ export declare function costResult(est: CostEstimate): CallToolResult;
|
|
|
46
190
|
* caller that stopped at a partial result believing it was complete would silently lose images,
|
|
47
191
|
* which is the failure this is meant to prevent, not cause.
|
|
48
192
|
*/
|
|
49
|
-
export declare function generationStatusResult(gen: Generation): CallToolResult;
|
|
193
|
+
export declare function generationStatusResult(gen: Generation, attachments?: GeneratedAttachment[], baseUrl?: string): CallToolResult;
|
|
50
194
|
/** One or more generations (snapshot or post-wait). Falls through to the single form for one id. */
|
|
51
|
-
export declare function generationBatchResult(gens: Generation[]): CallToolResult;
|
|
195
|
+
export declare function generationBatchResult(gens: Generation[], attachmentsByOutputId?: Record<string, GeneratedAttachment[]>, baseUrl?: string): CallToolResult;
|
|
52
196
|
/** A finished transcription: header line plus the transcript body. */
|
|
53
197
|
export declare function transcriptResult(t: Transcription): CallToolResult;
|
|
54
198
|
/** List of avatars, each with the fields an agent needs to drive lip-sync. */
|
package/dist/format.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
package/dist/format.js
CHANGED
|
@@ -3,15 +3,138 @@
|
|
|
3
3
|
* Tool errors are returned as `isError` results (not thrown) so the agent sees
|
|
4
4
|
* a readable message instead of a transport failure.
|
|
5
5
|
*/
|
|
6
|
+
// The spec's own spelling of the key that binds a result to its widget. See `server.ts` for why only the
|
|
7
|
+
// constants come from this package and not its server helpers.
|
|
8
|
+
import { RESOURCE_URI_META_KEY } from '@modelcontextprotocol/ext-apps';
|
|
9
|
+
import { GENERATION_WIDGET_URI } from './widget-uri.js';
|
|
6
10
|
import { ContentHeroError, InsufficientCreditsError, RateLimitError } from '@contenthero/sdk';
|
|
7
11
|
export function text(body, isError = false) {
|
|
8
12
|
return { content: [{ type: 'text', text: body }], isError };
|
|
9
13
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A finished generation: the asset URLs and placement outcome, plus the asset ITSELF.
|
|
16
|
+
*
|
|
17
|
+
* ## Why this used to be text only
|
|
18
|
+
*
|
|
19
|
+
* 🚨 **AN AGENT THAT GENERATES AN IMAGE COULD NOT SEE IT.** This returned a header, an outputId and a
|
|
20
|
+
* numbered list of urls, so the model had metadata and nothing else. Measured 2026-09-19 in a real ChatGPT
|
|
21
|
+
* and Claude session: Claude said "I can't see the image myself, only the metadata", and the link it
|
|
22
|
+
* surfaced was dead on arrival because ChatGPT had appended `utm_source=chatgpt.com` to a presigned URL and
|
|
23
|
+
* broken its signature.
|
|
24
|
+
*
|
|
25
|
+
* ⭐ The machinery already existed and was wired to the wrong tools: `mediaBatchResult` and
|
|
26
|
+
* `liveContextResult` have pushed image blocks for a while. Generation, the surface where a user most wants
|
|
27
|
+
* to SEE the result, was the one that did not.
|
|
28
|
+
*
|
|
29
|
+
* ## ⛔ THE AGENT CANNOT SEE WHAT IT MADE FROM THIS RESULT, AND THAT IS DELIBERATE
|
|
30
|
+
*
|
|
31
|
+
* An `image` block feeds the MODEL's vision; a `resource_link` gives the HOST something to render for the
|
|
32
|
+
* human. So the user sees every variation inline, and the model has a name and a url.
|
|
33
|
+
*
|
|
34
|
+
* ⭐ **`get_media` IS HOW A MODEL ACTUALLY LOOKS AT SOMETHING.** It embeds bytes as image blocks for exactly
|
|
35
|
+
* that purpose, so an agent that needs to judge a result (is the hand wrong, is the text legible, which of
|
|
36
|
+
* these four is best) calls it with the outputId. Embedding bytes HERE instead would spend the user's
|
|
37
|
+
* context on every generation to answer a question they may never ask, and a four-image batch would pay
|
|
38
|
+
* that cost four times over, repeated in every later turn.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ ATTACHMENTS ARE BUILT BY THE CALLER, not here. This module stays pure, the same split
|
|
41
|
+
* `mediaBatchResult` already uses: the handler decides, the formatter assembles.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* What the generation WIDGET reads.
|
|
45
|
+
*
|
|
46
|
+
* ⭐⭐ `structuredContent` IS THE WIDGET'S ONLY INPUT. It is a separate channel from `content`: the blocks
|
|
47
|
+
* feed the model and any host without app support, this feeds the UI. Both are emitted, so nothing regresses
|
|
48
|
+
* where widgets are unsupported and nothing is duplicated where they are.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ URLS ONLY, NEVER BYTES. The widget runs in the host's frame and fetches media itself, and our
|
|
51
|
+
* capability urls carry their token in the QUERY STRING, so `<video src>` loads one directly with no header
|
|
52
|
+
* to set. Embedding base64 here would pay the context cost twice over.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Where one output lives in the product.
|
|
56
|
+
*
|
|
57
|
+
* ## ⭐⭐⭐ COMPUTED HERE, NOT IN THE WIDGET, BECAUSE HERE THE INDEX IS ALREADY A NUMBER
|
|
58
|
+
*
|
|
59
|
+
* The widget's first version parsed the slot back out of the output's NAME (`<id>-3` means slot 2), which
|
|
60
|
+
* is a derivation of something this function has in its hand. Parsing a number out of a string we formatted
|
|
61
|
+
* two lines earlier is how an off-by-one gets in, and the symptom would be "Open shows the wrong picture",
|
|
62
|
+
* which reads as a broken link rather than an index bug.
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ **ONE-BASED IN THE NAME, ZERO-BASED IN THE URL.** `<id>-3` is what a person reads as "variation 3",
|
|
65
|
+
* and `variation=2` is the studio's `imageIndex`, which is slot space. Both conventions are correct in
|
|
66
|
+
* their own place and the conversion belongs at exactly one boundary, which is this one.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ A single-output generation gets NO `variation`. There is no variation to name, and passing 0 would
|
|
69
|
+
* imply there was a choice.
|
|
70
|
+
*/
|
|
71
|
+
/** Where a deep link points when no client base url was threaded through. */
|
|
72
|
+
export const DEFAULT_APP_URL = 'https://app.contenthero.ai';
|
|
73
|
+
export function studioUrlFor(baseUrl, outputId, index, total) {
|
|
74
|
+
const root = baseUrl.replace(/\/+$/, '');
|
|
75
|
+
const variation = total > 1 ? `&variation=${index}` : '';
|
|
76
|
+
return `${root}/studio?output=${encodeURIComponent(outputId)}${variation}`;
|
|
77
|
+
}
|
|
78
|
+
export function generationWidgetData(gen, posterUrls = [], baseUrl = DEFAULT_APP_URL) {
|
|
79
|
+
const urls = gen.outputUrls ?? [];
|
|
80
|
+
return {
|
|
81
|
+
outputId: gen.outputId,
|
|
82
|
+
contentType: gen.contentType,
|
|
83
|
+
modelId: gen.modelId,
|
|
84
|
+
/**
|
|
85
|
+
* ⛔⛔ **NULL WHEN THERE IS NOTHING TO NAME, AND THE WIDGET MUST RENDER NO CHIP.**
|
|
86
|
+
*
|
|
87
|
+
* This used to be `displayName ?? gen.modelId`, fed by a catalog fetch in the server with a `catch`
|
|
88
|
+
* that returned the id. The id reads like a label, so a failed fetch showed as a chip flickering
|
|
89
|
+
* between kebab case and title case rather than as a failure. The name now arrives on the row.
|
|
90
|
+
*
|
|
91
|
+
* ⚠️ It is not always this generation's own model: an upload names itself, and a look names the model
|
|
92
|
+
* that produced the image it was assembled from.
|
|
93
|
+
*/
|
|
94
|
+
modelName: gen.modelDisplayName ?? null,
|
|
95
|
+
/** Hex brand accent from the registry, or null. The widget colors its chip with it. */
|
|
96
|
+
modelBrandColor: gen.modelBrandColor ?? null,
|
|
97
|
+
/** Brand family key (e.g. `"openai"`) for choosing a glyph. NOT the model id. */
|
|
98
|
+
modelIconKey: gen.modelIconKey ?? null,
|
|
99
|
+
/** `"W:H"`, or null for audio. Drives the column count and the tile's own shape. */
|
|
100
|
+
displayAspect: gen.displayAspect ?? null,
|
|
101
|
+
/** Verbatim. Some prompts are JSON-shaped because the person authored one; that object IS the prompt. */
|
|
102
|
+
prompt: gen.prompt ?? null,
|
|
103
|
+
outputs: urls.map((url, i) => ({
|
|
104
|
+
url,
|
|
105
|
+
posterUrl: posterUrls[i] ?? null,
|
|
106
|
+
name: `${gen.outputId}${urls.length > 1 ? `-${i + 1}` : ''}`,
|
|
107
|
+
/** Where the Open button goes: this asset, in the studio, with its siblings and every action. */
|
|
108
|
+
studioUrl: studioUrlFor(baseUrl, gen.outputId, i, urls.length),
|
|
109
|
+
})),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function completedResult(gen, attachments = [], posterUrls = [],
|
|
113
|
+
/** The server this client talks to, so Open deep-links to it rather than always to production. */
|
|
114
|
+
baseUrl) {
|
|
12
115
|
const urls = gen.outputUrls ?? [];
|
|
13
116
|
const noun = urls.length === 1 ? gen.contentType : `${gen.contentType}s`;
|
|
14
|
-
|
|
117
|
+
/**
|
|
118
|
+
* ⭐⭐ **ONE TEXT LIST, AND NO `resource_link` BLOCKS. THREE REPRESENTATIONS OF ONE URL WAS TWO TOO MANY.**
|
|
119
|
+
*
|
|
120
|
+
* This went through both extremes before landing here. Listing the urls in prose AND attaching a link per
|
|
121
|
+
* output printed every url twice. Suppressing the prose left the links alone, and a host renders those as
|
|
122
|
+
* `name: uri` with NO SEPARATOR BETWEEN THEM, so url 1 ended flush against filename 2 and anything
|
|
123
|
+
* splitting on whitespace read a corrupted token. Measured at all three boundaries of a four-image batch.
|
|
124
|
+
*
|
|
125
|
+
* ⛔ THE SEPARATOR WAS NEVER OURS TO ADD. Our text block ends with a newline; the run-together is the host
|
|
126
|
+
* concatenating sibling blocks, and no content we emit can put a break between two of them.
|
|
127
|
+
*
|
|
128
|
+
* ⭐ So the fallback is the thing we fully control: one newline-delimited list. The widget is the surface
|
|
129
|
+
* that renders, and this is what a host without app support (or a model reading the transcript) gets. It
|
|
130
|
+
* costs less than the links did and it cannot be run together by anybody.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* ⭐ **THE ID IS AN ACCEPTABLE FALLBACK HERE AND NOWHERE ELSE.** This block's reader is the model, for
|
|
134
|
+
* whom `gpt-image-2` is a true and directly useful token. The widget's chip has a human reader, for whom
|
|
135
|
+
* the same string is an unexplained failure wearing a label's clothes, so there it renders as nothing.
|
|
136
|
+
*/
|
|
137
|
+
const header = `Done. ${urls.length} ${noun} from ${gen.modelDisplayName ?? gen.modelId} (outputId ${gen.outputId}):`;
|
|
15
138
|
const lines = [header, ...urls.map((u, i) => `${i + 1}. ${u}`)];
|
|
16
139
|
const p = gen.placement;
|
|
17
140
|
if (p) {
|
|
@@ -24,7 +147,34 @@ export function completedResult(gen) {
|
|
|
24
147
|
if (p.warnings?.length)
|
|
25
148
|
lines.push(`Placement notes: ${p.warnings.join('; ')}`);
|
|
26
149
|
}
|
|
27
|
-
|
|
150
|
+
// ⚠️ A TRAILING NEWLINE, because a host concatenates blocks without inserting one. Without it the last
|
|
151
|
+
// url ran straight into the next block's rendering, producing `...MBCo_Kkcfe3bafb-...-1.png: https://...`
|
|
152
|
+
// and a token that anything splitting on whitespace would read as part of the filename.
|
|
153
|
+
const content = [{ type: 'text', text: lines.join('\n') + '\n' }];
|
|
154
|
+
for (const a of attachments) {
|
|
155
|
+
if (a.kind === 'bytes') {
|
|
156
|
+
content.push({ type: a.type, data: a.data, mimeType: a.mimeType });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// A `resource_link` names the bytes without carrying them. The uri is a capability url, so it does not
|
|
160
|
+
// expire and appended query parameters cannot invalidate it.
|
|
161
|
+
content.push({ type: 'resource_link', uri: a.uri, name: a.name, mimeType: a.mimeType });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* ⭐⭐⭐ **THE `_meta` KEY IS WHAT MAKES A WIDGET APPEAR.** Without it the host has an HTML resource it was
|
|
166
|
+
* never told to mount, and the result renders as blocks alone. With it, a host that supports MCP Apps
|
|
167
|
+
* shows the widget and a host that does not ignores the key entirely.
|
|
168
|
+
*
|
|
169
|
+
* ⚠️ Emitted ALONGSIDE the blocks, never instead of them. Two mechanisms, one of which is better where it
|
|
170
|
+
* exists: ChatGPT and Claude get the widget, anything else still gets an image it can draw.
|
|
171
|
+
*/
|
|
172
|
+
return {
|
|
173
|
+
content,
|
|
174
|
+
isError: false,
|
|
175
|
+
structuredContent: generationWidgetData(gen, posterUrls, baseUrl),
|
|
176
|
+
_meta: { [RESOURCE_URI_META_KEY]: GENERATION_WIDGET_URI, ui: { resourceUri: GENERATION_WIDGET_URI } },
|
|
177
|
+
};
|
|
28
178
|
}
|
|
29
179
|
/** Suggested seconds to wait before re-polling a job, by content type. */
|
|
30
180
|
export function pollAfterSecondsFor(contentType) {
|
|
@@ -41,9 +191,52 @@ export function pollAfterSecondsFor(contentType) {
|
|
|
41
191
|
export function getStatusCall(outputIds) {
|
|
42
192
|
return `get_generation_status { outputIds: [${outputIds.map((id) => `"${id}"`).join(', ')}] }`;
|
|
43
193
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
194
|
+
/**
|
|
195
|
+
* A slow job that did not finish within the smart-wait window.
|
|
196
|
+
*
|
|
197
|
+
* ## ⭐⭐⭐ THIS IS WHERE THE SKELETONS COME FROM, AND WHY IT IS THE ONLY PLACE THEY COULD
|
|
198
|
+
*
|
|
199
|
+
* A generation returns one of two ways: it finished inside the smart wait, in which case there is nothing
|
|
200
|
+
* to show a spinner for, or it did not, and until now that produced ONE SENTENCE of prose asking the agent
|
|
201
|
+
* to poll. Every video takes that path. So the person who waited longest got the least: a paragraph, while
|
|
202
|
+
* the same job in the studio shows placeholder cards filling in.
|
|
203
|
+
*
|
|
204
|
+
* ⛔ **THE FIX IS NOT TO MAKE `generate_*` RETURN EARLY.** That would hand every host the pending path,
|
|
205
|
+
* including hosts with no MCP Apps support, which would lose the inline image they get today. The pending
|
|
206
|
+
* path already exists and already reaches exactly the people who are waiting.
|
|
207
|
+
*
|
|
208
|
+
* ⚠️ **THE TEXT STAYS, WORD FOR WORD.** It is what a host without app support renders, and it is what the
|
|
209
|
+
* AGENT reads to know it must poll. The widget is added ALONGSIDE it, not instead of it: an agent that
|
|
210
|
+
* stopped polling because the prose was replaced by a payload it cannot see would leave the generation
|
|
211
|
+
* unclaimed.
|
|
212
|
+
*
|
|
213
|
+
* ⚠️ No model NAME here, and that is deliberate. Resolving one would mean a second network call from
|
|
214
|
+
* inside a `catch`, which is the exact shape that produced a chip flickering between kebab case and title
|
|
215
|
+
* case. The widget polls `get_generation_status`, and the name arrives with the first response.
|
|
216
|
+
*/
|
|
217
|
+
export function pendingResult(outputId, pollAfterSeconds = 15, shape) {
|
|
218
|
+
const prose = `Still rendering (outputId ${outputId}). This is normal for video. Call ${getStatusCall([outputId])} in ~${pollAfterSeconds}s [poll_after_seconds: ${pollAfterSeconds}] to get the final URLs.`;
|
|
219
|
+
if (!shape)
|
|
220
|
+
return text(prose);
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: 'text', text: prose }],
|
|
223
|
+
structuredContent: {
|
|
224
|
+
outputId,
|
|
225
|
+
status: 'processing',
|
|
226
|
+
contentType: shape.contentType,
|
|
227
|
+
modelId: shape.modelId,
|
|
228
|
+
modelName: null,
|
|
229
|
+
modelBrandColor: null,
|
|
230
|
+
modelIconKey: null,
|
|
231
|
+
displayAspect: shape.displayAspect ?? null,
|
|
232
|
+
prompt: null,
|
|
233
|
+
/** At least one, or the widget renders a grid with nothing in it and looks broken rather than busy. */
|
|
234
|
+
expected: Math.max(1, shape.expected ?? 1),
|
|
235
|
+
pollAfterSeconds,
|
|
236
|
+
outputs: [],
|
|
237
|
+
},
|
|
238
|
+
_meta: { [RESOURCE_URI_META_KEY]: GENERATION_WIDGET_URI, ui: { resourceUri: GENERATION_WIDGET_URI } },
|
|
239
|
+
};
|
|
47
240
|
}
|
|
48
241
|
/** Synchronous audio result (already complete on submit). */
|
|
49
242
|
export function audioResult(result) {
|
|
@@ -96,9 +289,18 @@ export function costResult(est) {
|
|
|
96
289
|
* caller that stopped at a partial result believing it was complete would silently lose images,
|
|
97
290
|
* which is the failure this is meant to prevent, not cause.
|
|
98
291
|
*/
|
|
99
|
-
export function generationStatusResult(gen) {
|
|
292
|
+
export function generationStatusResult(gen, attachments = [], baseUrl) {
|
|
293
|
+
/**
|
|
294
|
+
* ⛔⛔ **THIS DROPPED THE ATTACHMENTS AND THEREFORE RENDERED NOTHING.** It called `completedResult(gen)`
|
|
295
|
+
* with no second argument, so POLLING returned text alone even after the generate handlers started
|
|
296
|
+
* attaching blocks. That is the path EVERY async generation takes, which is every video, so the common
|
|
297
|
+
* case stayed blank while the synchronous one worked.
|
|
298
|
+
*
|
|
299
|
+
* ⭐ Found by the `verify:inline` harness in its first run, minutes after it existed. The unit tests could
|
|
300
|
+
* not see it: they call `completedResult` directly and never go through here.
|
|
301
|
+
*/
|
|
100
302
|
if (gen.status === 'completed')
|
|
101
|
-
return completedResult(gen);
|
|
303
|
+
return completedResult(gen, attachments, [], baseUrl);
|
|
102
304
|
if (gen.status === 'failed') {
|
|
103
305
|
return text(`Generation ${gen.outputId} failed: ${gen.error ?? 'unknown error'}`, true);
|
|
104
306
|
}
|
|
@@ -116,9 +318,12 @@ export function generationStatusResult(gen) {
|
|
|
116
318
|
].join('\n'));
|
|
117
319
|
}
|
|
118
320
|
/** One or more generations (snapshot or post-wait). Falls through to the single form for one id. */
|
|
119
|
-
export function generationBatchResult(gens) {
|
|
321
|
+
export function generationBatchResult(gens, attachmentsByOutputId = {}, baseUrl) {
|
|
322
|
+
// ⚠️ ONLY THE SINGLE FORM ATTACHES. A batch status covering ten generations would embed ten sets of
|
|
323
|
+
// bytes into one result, which is the context blow-up the link design was originally protecting against.
|
|
324
|
+
// The single form is what a caller polling one generation hits, and that is the case worth rendering.
|
|
120
325
|
if (gens.length === 1)
|
|
121
|
-
return generationStatusResult(gens[0]);
|
|
326
|
+
return generationStatusResult(gens[0], attachmentsByOutputId[gens[0].outputId] ?? [], baseUrl);
|
|
122
327
|
const rows = gens.map((gen) => {
|
|
123
328
|
if (gen.status === 'completed') {
|
|
124
329
|
const urls = gen.outputUrls ?? [];
|
|
@@ -1133,7 +1338,7 @@ export function projectDetailResult(p) {
|
|
|
1133
1338
|
return text(`Project ${p.id}: "${p.title}" (${p.kind}, ${p.orientation} ${p.width}x${p.height}), revision ${p.revision}.\n` +
|
|
1134
1339
|
`Pass this revision back as expectedRevision when you edit.\n` +
|
|
1135
1340
|
// The output resolution above is NOT the coordinate space layer geometry uses. Stating both, adjacent
|
|
1136
|
-
// and
|
|
1341
|
+
// and labeled, is the point: an agent that read only "2168x1152" sized every layer 2.26x too large
|
|
1137
1342
|
// and got no error for it, because an oversized box is valid input.
|
|
1138
1343
|
(p.compositionSpace
|
|
1139
1344
|
? `Layer geometry is in composition space ${p.compositionSpace.width}x${p.compositionSpace.height} ` +
|