@hadialmarzooq/agent-media-mcp 0.2.0 → 0.3.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 (2) hide show
  1. package/dist/index.js +378 -80
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -11,12 +11,18 @@ import {
11
11
  parsePlan,
12
12
  planMedia,
13
13
  validatePlan,
14
- verifyMedia
14
+ verifyMedia,
15
+ inspectPlanIssues,
16
+ repairPlan,
17
+ parseReceipt,
18
+ mediaPlanSchemaId,
19
+ mediaPlanSchemaVersion
15
20
  } from "@hadialmarzooq/agent-media-core";
16
21
  import {
17
22
  concatenate,
18
23
  executePlan,
19
24
  extractAudio,
25
+ resumeFromReceipt,
20
26
  extractFrame,
21
27
  getCapabilities,
22
28
  inspectMedia,
@@ -54,7 +60,82 @@ function validateGoalSchema(goals) {
54
60
  }
55
61
  return goals;
56
62
  }
57
- var planRefSchema = z.object({ irVersion: z.literal("1"), source: z.object({ path: z.string() }) }).passthrough();
63
+ var planRefSchema = z.object({ irVersion: z.string().min(1), source: z.object({ path: z.string() }) }).passthrough();
64
+ var videoStreamSchema = z.object({
65
+ width: z.number(),
66
+ height: z.number(),
67
+ aspectRatio: z.string(),
68
+ fps: z.number().optional(),
69
+ codec: z.string().optional(),
70
+ pixelFormat: z.string().optional(),
71
+ rotationDegrees: z.number().optional()
72
+ });
73
+ var mediaMetadataShape = {
74
+ path: z.string(),
75
+ kind: z.enum(["video", "audio", "image", "unknown"]),
76
+ durationSeconds: z.number().optional(),
77
+ container: z.string().optional(),
78
+ sizeBytes: z.number(),
79
+ video: videoStreamSchema.optional(),
80
+ audio: z.object({
81
+ present: z.boolean(),
82
+ codec: z.string().optional(),
83
+ sampleRate: z.number().optional(),
84
+ channels: z.number().optional()
85
+ })
86
+ };
87
+ var mediaMetadataSchema = z.object(mediaMetadataShape);
88
+ var verificationShape = {
89
+ passed: z.boolean(),
90
+ checks: z.record(
91
+ z.string(),
92
+ z.object({
93
+ passed: z.boolean(),
94
+ expected: z.unknown(),
95
+ actual: z.unknown(),
96
+ message: z.string()
97
+ })
98
+ ),
99
+ failures: z.array(z.string()),
100
+ warnings: z.array(z.string())
101
+ };
102
+ var verificationSchema = z.object(verificationShape);
103
+ var planSchema = z.object({
104
+ irVersion: z.literal("1"),
105
+ source: z.object({ path: z.string() }),
106
+ constraints: z.record(z.string(), z.unknown()),
107
+ steps: z.array(z.record(z.string(), z.unknown())),
108
+ expectations: z.record(z.string(), z.unknown())
109
+ });
110
+ var echoedObject = z.looseObject({});
111
+ var receiptSchema = z.looseObject({ receiptVersion: z.string(), planFingerprint: z.string() });
112
+ var workflowResultShape = {
113
+ source: echoedObject,
114
+ plan: echoedObject,
115
+ output: mediaMetadataSchema,
116
+ verification: verificationSchema,
117
+ resumed: z.boolean().optional(),
118
+ receipt: receiptSchema.optional()
119
+ };
120
+ var contentChecksSchema = z.object({
121
+ blackFrames: z.union([z.boolean(), z.object({ minDurationSeconds: z.number().positive() })]).optional(),
122
+ silence: z.union([
123
+ z.boolean(),
124
+ z.object({
125
+ thresholdDb: z.number().optional(),
126
+ minDurationSeconds: z.number().positive().optional()
127
+ })
128
+ ]).optional(),
129
+ freeze: z.union([z.boolean(), z.object({ minDurationSeconds: z.number().positive() })]).optional(),
130
+ completeness: z.boolean().optional()
131
+ }).strict();
132
+ var contentCheckInputs = {
133
+ contentChecks: contentChecksSchema.optional().describe("Decode the output once and check what it contains, not just its shape."),
134
+ warnOnly: z.array(z.string().min(1)).optional().describe("Check names that warn instead of failing the call.")
135
+ };
136
+ function contentCheckOptions(value) {
137
+ return value === void 0 ? {} : { contentChecks: value };
138
+ }
58
139
  var readOnlyHint = { readOnlyHint: true };
59
140
  var destructiveHint = { destructiveHint: true };
60
141
  function createMcpServer() {
@@ -64,6 +145,7 @@ function createMcpServer() {
64
145
  {
65
146
  description: "Inspect normalized media metadata. Paths resolve against the server working directory; absolute paths are recommended.",
66
147
  inputSchema: { input: z.string().min(1) },
148
+ outputSchema: mediaMetadataShape,
67
149
  annotations: readOnlyHint
68
150
  },
69
151
  async ({ input }) => safely(async () => inspectMedia(input))
@@ -72,6 +154,22 @@ function createMcpServer() {
72
154
  "get_media_capabilities",
73
155
  {
74
156
  description: "Detect local FFmpeg capabilities.",
157
+ outputSchema: {
158
+ ffmpegVersion: z.string(),
159
+ encoders: z.object({
160
+ h264: z.boolean(),
161
+ hevc: z.boolean(),
162
+ av1: z.boolean(),
163
+ aac: z.boolean()
164
+ }),
165
+ hardwareAcceleration: z.array(z.string()),
166
+ filters: z.object({
167
+ scale: z.boolean(),
168
+ crop: z.boolean(),
169
+ concat: z.boolean(),
170
+ subtitles: z.boolean()
171
+ })
172
+ },
75
173
  annotations: readOnlyHint
76
174
  },
77
175
  async () => safely(getCapabilities)
@@ -79,8 +177,9 @@ function createMcpServer() {
79
177
  server.registerTool(
80
178
  "plan_media",
81
179
  {
82
- description: "Create an inspectable versioned semantic Media IR plan from semantic goals. All goals in the MediaGoals type are accepted.",
180
+ description: `Create an inspectable versioned semantic Media IR plan from semantic goals. All goals in the MediaGoals type are accepted. Plans conform to the canonical Media IR v${mediaPlanSchemaVersion} JSON Schema at ${mediaPlanSchemaId}, also returned by get_media_plan_schema.`,
83
181
  inputSchema: { input: z.string().min(1), goals: goalSchema },
182
+ outputSchema: { plan: planSchema },
84
183
  annotations: readOnlyHint
85
184
  },
86
185
  async ({ input, goals }) => safely(async () => ({
@@ -91,6 +190,80 @@ function createMcpServer() {
91
190
  })
92
191
  }))
93
192
  );
193
+ server.registerTool(
194
+ "validate_plan",
195
+ {
196
+ description: `Detect mechanical plan issues (impossible trims, dimension conflicts, out-of-range timestamps, concatenation stream conflicts) against a real source without executing. Plans conform to the canonical Media IR v${mediaPlanSchemaVersion} JSON Schema at ${mediaPlanSchemaId}. Read-only.`,
197
+ inputSchema: {
198
+ plan: z.union([z.string().min(1), planRefSchema])
199
+ },
200
+ outputSchema: {
201
+ issues: z.array(
202
+ z.object({
203
+ field: z.string(),
204
+ message: z.string(),
205
+ repairable: z.boolean(),
206
+ normalization: z.array(
207
+ z.object({
208
+ input: z.string(),
209
+ differences: z.array(z.string()),
210
+ plan: planSchema
211
+ })
212
+ ).optional()
213
+ })
214
+ )
215
+ },
216
+ annotations: readOnlyHint
217
+ },
218
+ async ({ plan: input }) => safely(async () => {
219
+ const plan = normalizePlan(input);
220
+ const source = await inspectMedia(plan.source.path);
221
+ const concatenationSources = await inspectConcatenationSources(plan, source);
222
+ return {
223
+ issues: inspectPlanIssues(plan, source, {
224
+ ...concatenationSources === void 0 ? {} : { concatenationSources }
225
+ })
226
+ };
227
+ })
228
+ );
229
+ server.registerTool(
230
+ "repair_plan",
231
+ {
232
+ description: `Repair mechanical plan issues (clamp trims and timestamps into source duration, reconcile resize with aspect ratio) and return the repaired plan with a structured repair report. Plans conform to the canonical Media IR v${mediaPlanSchemaVersion} JSON Schema at ${mediaPlanSchemaId}.`,
233
+ inputSchema: {
234
+ plan: z.union([z.string().min(1), planRefSchema])
235
+ },
236
+ outputSchema: {
237
+ repairs: z.array(
238
+ z.object({
239
+ field: z.string(),
240
+ action: z.string(),
241
+ from: z.unknown(),
242
+ to: z.unknown()
243
+ })
244
+ ),
245
+ repairedPlan: planSchema
246
+ },
247
+ annotations: readOnlyHint
248
+ },
249
+ async ({ plan: input }) => safely(async () => {
250
+ const plan = normalizePlan(input);
251
+ const { plan: repaired, repairs } = repairPlan(plan, await inspectMedia(plan.source.path));
252
+ return { repairs, repairedPlan: repaired };
253
+ })
254
+ );
255
+ server.registerTool(
256
+ "get_media_plan_schema",
257
+ {
258
+ description: `Return the canonical Media Plan JSON Schema (Media IR v${mediaPlanSchemaVersion}, ${mediaPlanSchemaId}) generated from the runtime models, so agent tooling cannot drift.`,
259
+ outputSchema: { $id: z.string(), schema: z.record(z.string(), z.unknown()) },
260
+ annotations: readOnlyHint
261
+ },
262
+ async () => safely(async () => {
263
+ const { mediaPlanJsonSchema } = await import("@hadialmarzooq/agent-media-core");
264
+ return { $id: mediaPlanSchemaId, schema: mediaPlanJsonSchema };
265
+ })
266
+ );
94
267
  server.registerTool(
95
268
  "make_vertical",
96
269
  {
@@ -104,26 +277,32 @@ function createMcpServer() {
104
277
  durationSeconds: z.number().positive().optional(),
105
278
  maxSizeMB: z.number().positive().optional(),
106
279
  audio: z.enum(["preserve", "remove"]).optional(),
107
- overwrite: z.boolean().optional()
280
+ overwrite: z.boolean().optional(),
281
+ ...contentCheckInputs
108
282
  },
283
+ outputSchema: workflowResultShape,
109
284
  annotations: destructiveHint
110
285
  },
111
286
  async (options, extra) => {
112
287
  const notifications = mcpProgress(extra);
113
288
  const response = await safely(
114
- async () => makeVertical({
115
- input: options.input,
116
- output: options.output,
117
- ...options.width === void 0 ? {} : { width: options.width },
118
- ...options.height === void 0 ? {} : { height: options.height },
119
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
120
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
121
- ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
122
- ...options.audio === void 0 ? {} : { audio: options.audio },
123
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
124
- signal: extra.signal,
125
- onProgress: notifications.notify
126
- })
289
+ async () => workflowResponse(
290
+ await makeVertical({
291
+ input: options.input,
292
+ output: options.output,
293
+ ...options.width === void 0 ? {} : { width: options.width },
294
+ ...options.height === void 0 ? {} : { height: options.height },
295
+ ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
296
+ ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
297
+ ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
298
+ ...options.audio === void 0 ? {} : { audio: options.audio },
299
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
300
+ ...contentCheckOptions(options.contentChecks),
301
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
302
+ signal: extra.signal,
303
+ onProgress: notifications.notify
304
+ })
305
+ )
127
306
  );
128
307
  await notifications.drain();
129
308
  return response;
@@ -141,25 +320,31 @@ function createMcpServer() {
141
320
  maxSizeMB: z.number().positive().optional(),
142
321
  quality: z.enum(["high", "balanced", "small"]).optional(),
143
322
  audio: z.enum(["preserve", "remove"]).optional(),
144
- overwrite: z.boolean().optional()
323
+ overwrite: z.boolean().optional(),
324
+ ...contentCheckInputs
145
325
  },
326
+ outputSchema: workflowResultShape,
146
327
  annotations: destructiveHint
147
328
  },
148
329
  async (options, extra) => {
149
330
  const notifications = mcpProgress(extra);
150
331
  const response = await safely(
151
- async () => optimizeForWeb({
152
- input: options.input,
153
- output: options.output,
154
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
155
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
156
- ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
157
- ...options.quality === void 0 ? {} : { quality: options.quality },
158
- ...options.audio === void 0 ? {} : { audio: options.audio },
159
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
160
- signal: extra.signal,
161
- onProgress: notifications.notify
162
- })
332
+ async () => workflowResponse(
333
+ await optimizeForWeb({
334
+ input: options.input,
335
+ output: options.output,
336
+ ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
337
+ ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
338
+ ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
339
+ ...options.quality === void 0 ? {} : { quality: options.quality },
340
+ ...options.audio === void 0 ? {} : { audio: options.audio },
341
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
342
+ ...contentCheckOptions(options.contentChecks),
343
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
344
+ signal: extra.signal,
345
+ onProgress: notifications.notify
346
+ })
347
+ )
163
348
  );
164
349
  await notifications.drain();
165
350
  return response;
@@ -175,23 +360,29 @@ function createMcpServer() {
175
360
  trimStartSeconds: z.number().nonnegative().optional(),
176
361
  durationSeconds: z.number().positive().optional(),
177
362
  audio: z.enum(["preserve", "remove"]).optional(),
178
- overwrite: z.boolean().optional()
363
+ overwrite: z.boolean().optional(),
364
+ ...contentCheckInputs
179
365
  },
366
+ outputSchema: workflowResultShape,
180
367
  annotations: destructiveHint
181
368
  },
182
369
  async (options, extra) => {
183
370
  const notifications = mcpProgress(extra);
184
371
  const response = await safely(
185
- async () => normalize({
186
- input: options.input,
187
- output: options.output,
188
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
189
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
190
- ...options.audio === void 0 ? {} : { audio: options.audio },
191
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
192
- signal: extra.signal,
193
- onProgress: notifications.notify
194
- })
372
+ async () => workflowResponse(
373
+ await normalize({
374
+ input: options.input,
375
+ output: options.output,
376
+ ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
377
+ ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
378
+ ...options.audio === void 0 ? {} : { audio: options.audio },
379
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
380
+ ...contentCheckOptions(options.contentChecks),
381
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
382
+ signal: extra.signal,
383
+ onProgress: notifications.notify
384
+ })
385
+ )
195
386
  );
196
387
  await notifications.drain();
197
388
  return response;
@@ -207,23 +398,29 @@ function createMcpServer() {
207
398
  format: z.enum(["m4a", "mp3", "wav"]).optional(),
208
399
  trimStartSeconds: z.number().nonnegative().optional(),
209
400
  durationSeconds: z.number().positive().optional(),
210
- overwrite: z.boolean().optional()
401
+ overwrite: z.boolean().optional(),
402
+ ...contentCheckInputs
211
403
  },
404
+ outputSchema: workflowResultShape,
212
405
  annotations: destructiveHint
213
406
  },
214
407
  async (options, extra) => {
215
408
  const notifications = mcpProgress(extra);
216
409
  const response = await safely(
217
- async () => extractAudio({
218
- input: options.input,
219
- output: options.output,
220
- ...options.format === void 0 ? {} : { format: options.format },
221
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
222
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
223
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
224
- signal: extra.signal,
225
- onProgress: notifications.notify
226
- })
410
+ async () => workflowResponse(
411
+ await extractAudio({
412
+ input: options.input,
413
+ output: options.output,
414
+ ...options.format === void 0 ? {} : { format: options.format },
415
+ ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
416
+ ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
417
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
418
+ ...contentCheckOptions(options.contentChecks),
419
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
420
+ signal: extra.signal,
421
+ onProgress: notifications.notify
422
+ })
423
+ )
227
424
  );
228
425
  await notifications.drain();
229
426
  return response;
@@ -238,22 +435,28 @@ function createMcpServer() {
238
435
  output: z.string().min(1),
239
436
  atSeconds: z.number().nonnegative().optional(),
240
437
  format: z.enum(["jpg", "png"]).optional(),
241
- overwrite: z.boolean().optional()
438
+ overwrite: z.boolean().optional(),
439
+ ...contentCheckInputs
242
440
  },
441
+ outputSchema: workflowResultShape,
243
442
  annotations: destructiveHint
244
443
  },
245
444
  async (options, extra) => {
246
445
  const notifications = mcpProgress(extra);
247
446
  const response = await safely(
248
- async () => extractFrame({
249
- input: options.input,
250
- output: options.output,
251
- ...options.atSeconds === void 0 ? {} : { atSeconds: options.atSeconds },
252
- ...options.format === void 0 ? {} : { format: options.format },
253
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
254
- signal: extra.signal,
255
- onProgress: notifications.notify
256
- })
447
+ async () => workflowResponse(
448
+ await extractFrame({
449
+ input: options.input,
450
+ output: options.output,
451
+ ...options.atSeconds === void 0 ? {} : { atSeconds: options.atSeconds },
452
+ ...options.format === void 0 ? {} : { format: options.format },
453
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
454
+ ...contentCheckOptions(options.contentChecks),
455
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
456
+ signal: extra.signal,
457
+ onProgress: notifications.notify
458
+ })
459
+ )
257
460
  );
258
461
  await notifications.drain();
259
462
  return response;
@@ -262,26 +465,30 @@ function createMcpServer() {
262
465
  server.registerTool(
263
466
  "concatenate_media",
264
467
  {
265
- description: "Concatenate multiple media sources into a single verified output. All inputs must have compatible stream layouts. Reports MCP progress when requested. Overwrite is destructive.",
468
+ description: "Concatenate two or more media sources into a single verified output. Pass every clip in `inputs`, in playback order. All inputs must have compatible stream layouts. Reports MCP progress when requested. Overwrite is destructive.",
266
469
  inputSchema: {
267
- input: z.string().min(1),
268
- inputs: z.array(z.string().min(1)).min(1),
470
+ inputs: z.array(z.string().min(1)).min(2).describe("Every clip to join, in playback order. The first entry leads the output."),
269
471
  output: z.string().min(1),
270
- overwrite: z.boolean().optional()
472
+ overwrite: z.boolean().optional(),
473
+ ...contentCheckInputs
271
474
  },
475
+ outputSchema: workflowResultShape,
272
476
  annotations: destructiveHint
273
477
  },
274
478
  async (options, extra) => {
275
479
  const notifications = mcpProgress(extra);
276
480
  const response = await safely(
277
- async () => concatenate({
278
- input: options.input,
279
- inputs: options.inputs,
280
- output: options.output,
281
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
282
- signal: extra.signal,
283
- onProgress: notifications.notify
284
- })
481
+ async () => workflowResponse(
482
+ await concatenate({
483
+ inputs: options.inputs,
484
+ output: options.output,
485
+ ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
486
+ ...contentCheckOptions(options.contentChecks),
487
+ ...options.warnOnly === void 0 ? {} : { warnOnly: options.warnOnly },
488
+ signal: extra.signal,
489
+ onProgress: notifications.notify
490
+ })
491
+ )
285
492
  );
286
493
  await notifications.drain();
287
494
  return response;
@@ -290,33 +497,102 @@ function createMcpServer() {
290
497
  server.registerTool(
291
498
  "execute_media_plan",
292
499
  {
293
- description: "Execute a serialized semantic Media IR plan. Accepts a plan object or JSON string. Overwrite is destructive.",
500
+ description: `Execute a serialized semantic Media IR plan conforming to the canonical Media IR v${mediaPlanSchemaVersion} JSON Schema at ${mediaPlanSchemaId}. Accepts a plan object or JSON string. Fails with VERIFICATION_FAILED when the output does not satisfy the plan. Overwrite is destructive. Set writeReceipt to emit a durable receipt, or resume to skip execution when a passing receipt already matches the plan and source.`,
294
501
  inputSchema: {
295
502
  plan: z.union([z.string().min(1), planRefSchema]),
296
503
  output: z.string().min(1),
297
- overwrite: z.boolean().optional()
504
+ overwrite: z.boolean().optional(),
505
+ writeReceipt: z.boolean().optional(),
506
+ resume: z.boolean().optional(),
507
+ ...contentCheckInputs
508
+ },
509
+ outputSchema: {
510
+ output: z.string(),
511
+ resumed: z.boolean().optional(),
512
+ receipt: receiptSchema.optional(),
513
+ verification: verificationSchema
298
514
  },
299
515
  annotations: destructiveHint
300
516
  },
301
- async ({ plan: input, output, overwrite }, extra) => {
517
+ async ({ plan: input, output, overwrite, writeReceipt, resume, contentChecks, warnOnly }, extra) => {
302
518
  const notifications = mcpProgress(extra);
303
519
  const response = await safely(async () => {
304
520
  const plan = normalizePlan(input);
305
521
  const execution = await executePlan(plan, {
306
522
  output,
307
523
  ...overwrite === void 0 ? {} : { overwrite },
524
+ ...writeReceipt === void 0 ? {} : { writeReceipt },
525
+ ...resume === void 0 ? {} : { resume },
526
+ ...contentCheckOptions(contentChecks),
527
+ ...warnOnly === void 0 ? {} : { warnOnly },
308
528
  signal: extra.signal,
309
529
  onProgress: notifications.notify
310
530
  });
531
+ const verification = execution.verification ?? execution.receipt?.verification ?? verifyMedia(await inspectMedia(execution.output), plan.expectations);
532
+ if (!verification.passed) {
533
+ throw new MediaError({
534
+ code: "VERIFICATION_FAILED",
535
+ message: "The plan executed, but the output did not satisfy its expectations.",
536
+ context: { output: execution.output, verification },
537
+ suggestedActions: ["Inspect the failed checks, adjust the plan, and retry."]
538
+ });
539
+ }
311
540
  return {
312
541
  output: execution.output,
313
- verification: verifyMedia(await inspectMedia(execution.output), plan.expectations)
542
+ ...execution.resumed === void 0 ? {} : { resumed: execution.resumed },
543
+ ...execution.receipt === void 0 ? {} : { receipt: execution.receipt },
544
+ verification
314
545
  };
315
546
  });
316
547
  await notifications.drain();
317
548
  return response;
318
549
  }
319
550
  );
551
+ server.registerTool(
552
+ "resume_execution",
553
+ {
554
+ description: "Continue from a saved execution receipt: skip the work when the recorded output still satisfies the same plan against an unchanged source, and re-execute the plan when it does not. Overwrite is destructive.",
555
+ inputSchema: {
556
+ receipt: z.string().min(1),
557
+ output: z.string().min(1).optional(),
558
+ overwrite: z.boolean().optional()
559
+ },
560
+ outputSchema: {
561
+ output: z.string(),
562
+ resumed: z.boolean(),
563
+ receipt: receiptSchema.optional()
564
+ },
565
+ annotations: destructiveHint
566
+ },
567
+ async ({ receipt, output, overwrite }, extra) => {
568
+ const notifications = mcpProgress(extra);
569
+ const response = await safely(async () => {
570
+ const execution = await resumeFromReceipt(parseReceipt(receipt), {
571
+ ...output === void 0 ? {} : { output },
572
+ ...overwrite === void 0 ? {} : { overwrite },
573
+ signal: extra.signal,
574
+ onProgress: notifications.notify
575
+ });
576
+ return {
577
+ output: execution.output,
578
+ resumed: execution.resumed === true,
579
+ ...execution.receipt === void 0 ? {} : { receipt: execution.receipt }
580
+ };
581
+ });
582
+ await notifications.drain();
583
+ return response;
584
+ }
585
+ );
586
+ server.registerTool(
587
+ "inspect_receipt",
588
+ {
589
+ description: "Validate and inspect a saved execution receipt (durable record of plan, source fingerprint, output, and verification).",
590
+ inputSchema: { receipt: z.string().min(1) },
591
+ outputSchema: { receipt: receiptSchema },
592
+ annotations: readOnlyHint
593
+ },
594
+ async ({ receipt }) => safely(async () => ({ receipt: parseReceipt(receipt) }))
595
+ );
320
596
  server.registerTool(
321
597
  "verify_media",
322
598
  {
@@ -325,6 +601,7 @@ function createMcpServer() {
325
601
  output: z.string().min(1),
326
602
  plan: z.union([z.string().min(1), planRefSchema])
327
603
  },
604
+ outputSchema: verificationShape,
328
605
  annotations: readOnlyHint
329
606
  },
330
607
  async ({ output, plan: input }) => safely(async () => {
@@ -334,8 +611,17 @@ function createMcpServer() {
334
611
  );
335
612
  return server;
336
613
  }
614
+ function workflowResponse(result2) {
615
+ const rest = { ...result2 };
616
+ delete rest.serializedPlan;
617
+ return rest;
618
+ }
337
619
  function result(value) {
338
- return { content: [{ type: "text", text: JSON.stringify(value) }] };
620
+ const content = [{ type: "text", text: JSON.stringify(value) }];
621
+ return isPlainObject(value) ? { content, structuredContent: value } : { content };
622
+ }
623
+ function isPlainObject(value) {
624
+ return typeof value === "object" && value !== null && !Array.isArray(value);
339
625
  }
340
626
  async function safely(operation) {
341
627
  try {
@@ -345,8 +631,20 @@ async function safely(operation) {
345
631
  code: "UNEXPECTED_ERROR",
346
632
  message: error instanceof Error ? error.message : String(error)
347
633
  };
348
- return { ...result(structured), isError: true };
634
+ return {
635
+ content: [{ type: "text", text: JSON.stringify(structured) }],
636
+ isError: true
637
+ };
638
+ }
639
+ }
640
+ async function inspectConcatenationSources(plan, source) {
641
+ const concatenate2 = plan.steps.find((step) => step.operation === "concatenate");
642
+ if (concatenate2?.operation !== "concatenate") return void 0;
643
+ const sources = [];
644
+ for (const [index, input] of concatenate2.inputs.entries()) {
645
+ sources.push(index === 0 ? source : await inspectMedia(input));
349
646
  }
647
+ return sources;
350
648
  }
351
649
  function normalizePlan(input) {
352
650
  return typeof input === "string" ? parsePlan(input) : validatePlan(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadialmarzooq/agent-media-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for semantic, replayable, and verified media transformations.",
5
5
  "keywords": [
6
6
  "media",
@@ -39,8 +39,8 @@
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "1.30.0",
41
41
  "zod": "^4.5.4",
42
- "@hadialmarzooq/agent-media-core": "0.2.0",
43
- "@hadialmarzooq/agent-media-ffmpeg": "0.2.0"
42
+ "@hadialmarzooq/agent-media-core": "0.3.0",
43
+ "@hadialmarzooq/agent-media-ffmpeg": "0.3.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup src/index.ts --format esm --dts",