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