@hubfluencer/mcp 0.4.0 → 0.6.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.
@@ -0,0 +1,360 @@
1
+ /**
2
+ * `outputSchema`s for the STABLE tool envelopes — paired with the
3
+ * `structuredContent` each of those tools already returns.
4
+ *
5
+ * WHY THESE LIVE HERE (not inline in index.ts):
6
+ * 1. index.ts boots a live stdio MCP server at module load, so it can't be
7
+ * imported from a unit test (see core.ts header). Declaring the schemas here
8
+ * lets test/output-schemas.test.ts validate each one against a representative
9
+ * `ok()` payload — a mismatch between a schema and the structuredContent it
10
+ * guards throws an McpError at call time (the SDK runs
11
+ * `safeParseAsync(outputSchema, structuredContent)` on every result), which
12
+ * breaks strict clients, so it must be caught in CI, not in production.
13
+ * 2. One place to keep the schema and its payload in lock-step.
14
+ *
15
+ * WHAT THE SDK DOES WITH THEM: `McpServer.registerTool` accepts a Zod schema (or
16
+ * raw shape) as `outputSchema`; it converts it to draft-07 JSON Schema for the
17
+ * `tools/list` wire (so clients see typed output) and validates every non-error
18
+ * result's `structuredContent` against it. There is NO raw-JSON-Schema path — a
19
+ * plain object would be rejected at registration — so these are Zod objects,
20
+ * exactly like every `inputSchema` in index.ts.
21
+ *
22
+ * DESIGN — permissive by construction (the schema must NEVER reject a real
23
+ * payload):
24
+ * - Every top-level object is `.passthrough()`. On the wire that emits
25
+ * `additionalProperties: {}` (JSON Schema draft-07 "any extra key allowed"),
26
+ * and at runtime it keeps unknown keys instead of failing — so a server that
27
+ * adds a field, or a handler that threads an extra `note`/`kind_detail`, never
28
+ * trips validation.
29
+ * - Fields the MCP layer itself constructs with a guaranteed type (the
30
+ * normalizeStatus booleans, a handler-set `slug`/`charged`/`saved_to`) are
31
+ * typed precisely — that's the useful signal for a client.
32
+ * - Fields passed THROUGH from server data whose type can legitimately vary
33
+ * (credit counts, captions, hashtags, nested items, whole server aggregates)
34
+ * are `z.unknown()` / permissive arrays. A too-strict type here would be the
35
+ * one thing that throws.
36
+ * - No field is `.required()` unless the handler ALWAYS sets it, and even then
37
+ * we keep the set minimal so an unusual response can't fail the required check.
38
+ *
39
+ * These mirror the exact `ok(...)` shapes in index.ts; a drift on either side is a
40
+ * bug the paired test is meant to catch.
41
+ */
42
+
43
+ import { z } from "zod";
44
+
45
+ /** A presigned URL or a local-null field that may be absent/variable. */
46
+ const urlish = z.unknown().optional();
47
+
48
+ // ── Status envelopes (normalizeStatus output) ────────────────────────────────
49
+ //
50
+ // get_status returns NormalizedStatus verbatim; wait_for_completion adds
51
+ // {saved_to, timed_out}. Every field except video_url/error is set by
52
+ // normalizeStatus with a guaranteed type (stage falls back to "unknown"), so
53
+ // those are typed precisely; video_url/error are string|null.
54
+
55
+ /** get_status → the normalized {kind, slug, stage, terminal, ready, …} shape. */
56
+ export const getStatusOutput = z
57
+ .object({
58
+ kind: z.string().optional(),
59
+ slug: z.string().optional(),
60
+ stage: z.string().optional(),
61
+ terminal: z.boolean().optional(),
62
+ ready: z.boolean().optional(),
63
+ video_url: z.union([z.string(), z.null()]).optional(),
64
+ error: z.union([z.string(), z.null()]).optional(),
65
+ })
66
+ .passthrough();
67
+
68
+ /** wait_for_completion → get_status plus the download/timeout bookkeeping. */
69
+ export const waitForCompletionOutput = z
70
+ .object({
71
+ kind: z.string().optional(),
72
+ slug: z.string().optional(),
73
+ stage: z.string().optional(),
74
+ terminal: z.boolean().optional(),
75
+ ready: z.boolean().optional(),
76
+ video_url: z.union([z.string(), z.null()]).optional(),
77
+ error: z.union([z.string(), z.null()]).optional(),
78
+ saved_to: z.union([z.string(), z.null()]).optional(),
79
+ timed_out: z.boolean().optional(),
80
+ })
81
+ .passthrough();
82
+
83
+ // ── Credits ──────────────────────────────────────────────────────────────────
84
+
85
+ /** get_credits → the whitelisted balance fields (server values, kept permissive). */
86
+ export const getCreditsOutput = z
87
+ .object({
88
+ credits: z.unknown().optional(),
89
+ spendable_credits: z.unknown().optional(),
90
+ reserved_credits: z.unknown().optional(),
91
+ remaining_reserved_credits: z.unknown().optional(),
92
+ message: z.unknown().optional(),
93
+ })
94
+ .passthrough();
95
+
96
+ // ── Download ───────────────────────────────────────────────────────────────────
97
+
98
+ /** download_result → the presigned URL and, when saved, the path + byte count. */
99
+ export const downloadResultOutput = z
100
+ .object({
101
+ video_url: z.union([z.string(), z.null()]).optional(),
102
+ saved_to: z.union([z.string(), z.null()]).optional(),
103
+ bytes: z.number().optional(),
104
+ note: z.string().optional(),
105
+ })
106
+ .passthrough();
107
+
108
+ // ── Sliders (carousels) ────────────────────────────────────────────────────────
109
+
110
+ /** One slide inside the get_slider deliverable (server-sourced fields → permissive). */
111
+ const sliderSlide = z
112
+ .object({
113
+ position: z.unknown().optional(),
114
+ image_url: urlish,
115
+ headline: z.unknown().optional(),
116
+ body: z.unknown().optional(),
117
+ kicker: z.unknown().optional(),
118
+ status: z.unknown().optional(),
119
+ })
120
+ .passthrough();
121
+
122
+ /** get_slider → carousel status + the per-slide deliverable. */
123
+ export const getSliderOutput = z
124
+ .object({
125
+ slug: z.string().optional(),
126
+ kind: z.string().optional(),
127
+ status: z.string().optional(),
128
+ stage: z.string().optional(),
129
+ terminal: z.boolean().optional(),
130
+ completed: z.boolean().optional(),
131
+ error: z.union([z.string(), z.null()]).optional(),
132
+ caption: z.union([z.string(), z.null()]).optional(),
133
+ hashtags: z.array(z.unknown()).optional(),
134
+ slides: z.array(sliderSlide).optional(),
135
+ })
136
+ .passthrough();
137
+
138
+ // ── Projects listing ────────────────────────────────────────────────────────────
139
+
140
+ /** A normalized status row inside list_projects (permissive — server-derived). */
141
+ const projectRow = z
142
+ .object({
143
+ kind: z.string().optional(),
144
+ slug: z.string().optional(),
145
+ stage: z.string().optional(),
146
+ terminal: z.boolean().optional(),
147
+ ready: z.boolean().optional(),
148
+ video_url: z.union([z.string(), z.null()]).optional(),
149
+ error: z.union([z.string(), z.null()]).optional(),
150
+ })
151
+ .passthrough();
152
+
153
+ /** list_projects → the two normalized lanes plus an optional truncation note. */
154
+ export const listProjectsOutput = z
155
+ .object({
156
+ shorts: z.array(projectRow).optional(),
157
+ projects: z.array(projectRow).optional(),
158
+ note: z.string().optional(),
159
+ })
160
+ .passthrough();
161
+
162
+ // ── Create-tool results ────────────────────────────────────────────────────────
163
+ //
164
+ // The draft-create tools all return a small {slug, kind, next?} envelope; the
165
+ // autopilot creators (create_editor_ad, create_editor_draft) also nest a
166
+ // normalizeStatus `status`. make_video returns the richest shape (a full status
167
+ // spread plus pricing/charge bookkeeping, OR — on dry_run / over-cap / short
168
+ // balance — a not-charged pricing envelope); one permissive schema covers both
169
+ // of its branches.
170
+
171
+ /** create_short / create_slider → the draft slug + a next-step hint. */
172
+ export const createDraftOutput = z
173
+ .object({
174
+ slug: z.union([z.string(), z.null()]).optional(),
175
+ kind: z.string().optional(),
176
+ next: z.string().optional(),
177
+ })
178
+ .passthrough();
179
+
180
+ /** create_editor_ad / create_editor_draft → the draft slug + nested status. */
181
+ export const createEditorOutput = z
182
+ .object({
183
+ slug: z.union([z.string(), z.null()]).optional(),
184
+ kind: z.string().optional(),
185
+ status: projectRow.optional(),
186
+ next: z.string().optional(),
187
+ })
188
+ .passthrough();
189
+
190
+ /** make_video → the finished/timed-out status + pricing, or a not-charged draft. */
191
+ export const makeVideoOutput = z
192
+ .object({
193
+ slug: z.union([z.string(), z.null()]).optional(),
194
+ kind: z.string().optional(),
195
+ kind_inferred: z.boolean().optional(),
196
+ stage: z.string().optional(),
197
+ terminal: z.boolean().optional(),
198
+ ready: z.boolean().optional(),
199
+ video_url: z.union([z.string(), z.null()]).optional(),
200
+ error: z.union([z.string(), z.null()]).optional(),
201
+ estimated_credits: z.unknown().optional(),
202
+ available_credits: z.unknown().optional(),
203
+ affordable: z.boolean().optional(),
204
+ charged: z.boolean().optional(),
205
+ saved_to: z.union([z.string(), z.null()]).optional(),
206
+ timed_out: z.boolean().optional(),
207
+ note: z.string().optional(),
208
+ })
209
+ .passthrough();
210
+
211
+ // ── Campaign engine (high-value new tools) ─────────────────────────────────────
212
+
213
+ /** A summarized campaign-pack item (summarizePackItem) — server fields permissive. */
214
+ const packItem = z
215
+ .object({
216
+ id: z.unknown().optional(),
217
+ kind: z.unknown().optional(),
218
+ status: z.unknown().optional(),
219
+ hook: z.unknown().optional(),
220
+ video_factory_slug: z.union([z.string(), z.null()]).optional(),
221
+ slider_slug: z.union([z.string(), z.null()]).optional(),
222
+ draft_slug: z.union([z.string(), z.null()]).optional(),
223
+ draft_route: z.union([z.string(), z.null()]).optional(),
224
+ credit_estimate: z.unknown().optional(),
225
+ preview_url: z.unknown().optional(),
226
+ video_result_id: z.unknown().optional(),
227
+ })
228
+ .passthrough();
229
+
230
+ /** get_campaign_pack → the summarized pack (summarizePack) with its items. */
231
+ export const getCampaignPackOutput = z
232
+ .object({
233
+ id: z.unknown().optional(),
234
+ title: z.unknown().optional(),
235
+ status: z.unknown().optional(),
236
+ source_type: z.unknown().optional(),
237
+ source_url: z.unknown().optional(),
238
+ brief: z.unknown().optional(),
239
+ product_profile_id: z.unknown().optional(),
240
+ brand_profile_id: z.unknown().optional(),
241
+ item_count: z.number().optional(),
242
+ items: z.array(packItem).optional(),
243
+ })
244
+ .passthrough();
245
+
246
+ /** create_campaign → the orchestrator result (runCreateCampaign / CreateCampaignResult). */
247
+ export const createCampaignOutput = z
248
+ .object({
249
+ product_profile_id: z.union([z.number(), z.null()]).optional(),
250
+ campaign_pack_id: z.union([z.number(), z.null()]).optional(),
251
+ brand_profile_id: z.union([z.number(), z.null()]).optional(),
252
+ extracted: z
253
+ .union([
254
+ z
255
+ .object({
256
+ source_url: z.union([z.string(), z.null()]).optional(),
257
+ confidence: z.unknown().optional(),
258
+ name: z.unknown().optional(),
259
+ })
260
+ .passthrough(),
261
+ z.null(),
262
+ ])
263
+ .optional(),
264
+ imported: z
265
+ .object({
266
+ primary_image: z.boolean().optional(),
267
+ logo: z.boolean().optional(),
268
+ })
269
+ .passthrough()
270
+ .optional(),
271
+ items: z.array(packItem).optional(),
272
+ summary: z
273
+ .object({
274
+ created: z.number().optional(),
275
+ materialized: z.number().optional(),
276
+ planned: z.number().optional(),
277
+ })
278
+ .passthrough()
279
+ .optional(),
280
+ steps: z
281
+ .array(
282
+ z
283
+ .object({
284
+ step: z.string().optional(),
285
+ ok: z.boolean().optional(),
286
+ detail: z.string().optional(),
287
+ })
288
+ .passthrough(),
289
+ )
290
+ .optional(),
291
+ next: z.string().optional(),
292
+ note: z.string().optional(),
293
+ })
294
+ .passthrough();
295
+
296
+ // ── Series + calendar ──────────────────────────────────────────────────────────
297
+
298
+ /** A summarized episode row inside the dashboard lanes (server fields permissive). */
299
+ const episodeRow = z
300
+ .object({
301
+ id: z.unknown().optional(),
302
+ status: z.unknown().optional(),
303
+ draft_slug: z.union([z.string(), z.null()]).optional(),
304
+ draft_route: z.union([z.string(), z.null()]).optional(),
305
+ })
306
+ .passthrough();
307
+
308
+ /** get_series_dashboard → the series + its lanes + per-item performance notes. */
309
+ export const getSeriesDashboardOutput = z
310
+ .object({
311
+ series: z.record(z.string(), z.unknown()).optional(),
312
+ upcoming: z.array(episodeRow).optional(),
313
+ drafted: z.array(episodeRow).optional(),
314
+ published: z.array(episodeRow).optional(),
315
+ performance_notes: z.array(z.unknown()).optional(),
316
+ })
317
+ .passthrough();
318
+
319
+ /** One summarized scheduled post (summarizeScheduledPost) — server fields permissive. */
320
+ const scheduledPost = z
321
+ .object({
322
+ id: z.unknown().optional(),
323
+ platform: z.unknown().optional(),
324
+ status: z.unknown().optional(),
325
+ video_result_id: z.unknown().optional(),
326
+ video_url: z.unknown().optional(),
327
+ video_factory_kind: z.unknown().optional(),
328
+ campaign_pack_item_id: z.unknown().optional(),
329
+ caption: z.unknown().optional(),
330
+ hashtags: z.unknown().optional(),
331
+ scheduled_at: z.unknown().optional(),
332
+ timezone: z.unknown().optional(),
333
+ posted_at: z.unknown().optional(),
334
+ published_at: z.unknown().optional(),
335
+ error_message: z.unknown().optional(),
336
+ })
337
+ .passthrough();
338
+
339
+ /** list_scheduled_posts → the calendar list. */
340
+ export const listScheduledPostsOutput = z
341
+ .object({
342
+ posts: z.array(scheduledPost).optional(),
343
+ })
344
+ .passthrough();
345
+
346
+ // ── Performance ────────────────────────────────────────────────────────────────
347
+
348
+ /**
349
+ * get_recommendations → the server aggregate passed through UNCHANGED. Its exact
350
+ * fields (top hook/format/language, best items, confidence, note) are the
351
+ * server's to state and can evolve, so this stays a permissive open object —
352
+ * enough to signal "structured object" to a client without ever rejecting the
353
+ * aggregate.
354
+ */
355
+ export const getRecommendationsOutput = z
356
+ .object({
357
+ confidence: z.unknown().optional(),
358
+ note: z.unknown().optional(),
359
+ })
360
+ .passthrough();