@hadialmarzooq/agent-media-mcp 0.1.1 → 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 +450 -92
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -8,26 +8,33 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  import {
10
10
  MediaError,
11
- mediaPlanSchema,
12
11
  parsePlan,
13
12
  planMedia,
14
13
  validatePlan,
15
- verifyMedia
14
+ verifyMedia,
15
+ inspectPlanIssues,
16
+ repairPlan,
17
+ parseReceipt,
18
+ mediaPlanSchemaId,
19
+ mediaPlanSchemaVersion
16
20
  } from "@hadialmarzooq/agent-media-core";
17
21
  import {
22
+ concatenate,
18
23
  executePlan,
24
+ extractAudio,
25
+ resumeFromReceipt,
26
+ extractFrame,
19
27
  getCapabilities,
20
28
  inspectMedia,
21
29
  makeVertical,
22
- optimizeForWeb,
23
30
  normalize,
24
- extractAudio,
25
- extractFrame
31
+ optimizeForWeb
26
32
  } from "@hadialmarzooq/agent-media-ffmpeg";
27
33
  import { z } from "zod";
28
34
  var packageVersion = createRequire(import.meta.url)("../package.json").version;
29
35
  var goalSchema = z.object({
30
36
  trimStartSeconds: z.number().nonnegative().optional(),
37
+ trimEndSeconds: z.number().finite().optional(),
31
38
  durationSeconds: z.number().positive().optional(),
32
39
  aspectRatio: z.string().optional(),
33
40
  width: z.number().int().positive().optional(),
@@ -35,41 +42,232 @@ var goalSchema = z.object({
35
42
  maxSizeMB: z.number().positive().optional(),
36
43
  compatibility: z.enum(["high", "balanced"]).optional(),
37
44
  quality: z.enum(["high", "balanced", "small"]).optional(),
38
- audio: z.enum(["preserve", "remove"]).optional()
45
+ audio: z.enum(["preserve", "remove"]).optional(),
46
+ extractAudio: z.object({ format: z.enum(["m4a", "mp3", "wav"]).optional() }).optional(),
47
+ extractFrame: z.object({
48
+ atSeconds: z.number().nonnegative().optional(),
49
+ format: z.enum(["jpg", "png"]).optional()
50
+ }).optional(),
51
+ concatenate: z.array(z.string().min(1)).min(1).optional()
52
+ }).strict();
53
+ function validateGoalSchema(goals) {
54
+ if (!Object.values(goals).some((v) => v !== void 0)) {
55
+ throw new MediaError({
56
+ code: "INVALID_PLAN",
57
+ message: "At least one goal must be provided. An empty goals object produces a no-op plan.",
58
+ suggestedActions: ["Provide at least one semantic goal."]
59
+ });
60
+ }
61
+ return goals;
62
+ }
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()
39
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
+ }
139
+ var readOnlyHint = { readOnlyHint: true };
140
+ var destructiveHint = { destructiveHint: true };
40
141
  function createMcpServer() {
41
142
  const server = new McpServer({ name: "agent-media", version: packageVersion });
42
143
  server.registerTool(
43
144
  "inspect_media",
44
145
  {
45
- description: "Inspect normalized media metadata.",
46
- inputSchema: { input: z.string().min(1) }
146
+ description: "Inspect normalized media metadata. Paths resolve against the server working directory; absolute paths are recommended.",
147
+ inputSchema: { input: z.string().min(1) },
148
+ outputSchema: mediaMetadataShape,
149
+ annotations: readOnlyHint
47
150
  },
48
151
  async ({ input }) => safely(async () => inspectMedia(input))
49
152
  );
50
153
  server.registerTool(
51
154
  "get_media_capabilities",
52
- { description: "Detect local FFmpeg capabilities." },
155
+ {
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
+ },
173
+ annotations: readOnlyHint
174
+ },
53
175
  async () => safely(getCapabilities)
54
176
  );
55
177
  server.registerTool(
56
178
  "plan_media",
57
179
  {
58
- description: "Create an inspectable versioned semantic Media IR plan.",
59
- inputSchema: { input: z.string().min(1), goals: goalSchema }
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.`,
181
+ inputSchema: { input: z.string().min(1), goals: goalSchema },
182
+ outputSchema: { plan: planSchema },
183
+ annotations: readOnlyHint
60
184
  },
61
185
  async ({ input, goals }) => safely(async () => ({
62
186
  plan: planMedia({
63
187
  source: await inspectMedia(input),
64
- goals: cleanGoals(goals),
188
+ goals: cleanGoals(validateGoalSchema(goals)),
65
189
  capabilities: await getCapabilities()
66
190
  })
67
191
  }))
68
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
+ );
69
267
  server.registerTool(
70
268
  "make_vertical",
71
269
  {
72
- description: "Inspect, plan, execute, and verify a high-compatibility 9:16 video. Reports MCP progress when requested.",
270
+ description: "Inspect, plan, execute, and verify a high-compatibility 9:16 video. Reports MCP progress when requested. Overwrite is destructive.",
73
271
  inputSchema: {
74
272
  input: z.string().min(1),
75
273
  output: z.string().min(1),
@@ -79,25 +277,32 @@ function createMcpServer() {
79
277
  durationSeconds: z.number().positive().optional(),
80
278
  maxSizeMB: z.number().positive().optional(),
81
279
  audio: z.enum(["preserve", "remove"]).optional(),
82
- overwrite: z.boolean().optional()
83
- }
280
+ overwrite: z.boolean().optional(),
281
+ ...contentCheckInputs
282
+ },
283
+ outputSchema: workflowResultShape,
284
+ annotations: destructiveHint
84
285
  },
85
286
  async (options, extra) => {
86
287
  const notifications = mcpProgress(extra);
87
288
  const response = await safely(
88
- async () => makeVertical({
89
- input: options.input,
90
- output: options.output,
91
- ...options.width === void 0 ? {} : { width: options.width },
92
- ...options.height === void 0 ? {} : { height: options.height },
93
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
94
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
95
- ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
96
- ...options.audio === void 0 ? {} : { audio: options.audio },
97
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
98
- signal: extra.signal,
99
- onProgress: notifications.notify
100
- })
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
+ )
101
306
  );
102
307
  await notifications.drain();
103
308
  return response;
@@ -106,7 +311,7 @@ function createMcpServer() {
106
311
  server.registerTool(
107
312
  "optimize_for_web",
108
313
  {
109
- description: "Inspect, plan, execute, and verify a web-optimized high-compatibility video. Reports MCP progress when requested.",
314
+ description: "Inspect, plan, execute, and verify a web-optimized high-compatibility video. Reports MCP progress when requested. Overwrite is destructive.",
110
315
  inputSchema: {
111
316
  input: z.string().min(1),
112
317
  output: z.string().min(1),
@@ -115,24 +320,31 @@ function createMcpServer() {
115
320
  maxSizeMB: z.number().positive().optional(),
116
321
  quality: z.enum(["high", "balanced", "small"]).optional(),
117
322
  audio: z.enum(["preserve", "remove"]).optional(),
118
- overwrite: z.boolean().optional()
119
- }
323
+ overwrite: z.boolean().optional(),
324
+ ...contentCheckInputs
325
+ },
326
+ outputSchema: workflowResultShape,
327
+ annotations: destructiveHint
120
328
  },
121
329
  async (options, extra) => {
122
330
  const notifications = mcpProgress(extra);
123
331
  const response = await safely(
124
- async () => optimizeForWeb({
125
- input: options.input,
126
- output: options.output,
127
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
128
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
129
- ...options.maxSizeMB === void 0 ? {} : { maxSizeMB: options.maxSizeMB },
130
- ...options.quality === void 0 ? {} : { quality: options.quality },
131
- ...options.audio === void 0 ? {} : { audio: options.audio },
132
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
133
- signal: extra.signal,
134
- onProgress: notifications.notify
135
- })
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
+ )
136
348
  );
137
349
  await notifications.drain();
138
350
  return response;
@@ -141,29 +353,36 @@ function createMcpServer() {
141
353
  server.registerTool(
142
354
  "normalize_media",
143
355
  {
144
- description: "Inspect, plan, execute, and verify a normalized high-compatibility copy (H.264, yuv420p, faststart). Reports MCP progress when requested.",
356
+ description: "Inspect, plan, execute, and verify a normalized high-compatibility copy (H.264, yuv420p, faststart). Reports MCP progress when requested. Overwrite is destructive.",
145
357
  inputSchema: {
146
358
  input: z.string().min(1),
147
359
  output: z.string().min(1),
148
360
  trimStartSeconds: z.number().nonnegative().optional(),
149
361
  durationSeconds: z.number().positive().optional(),
150
362
  audio: z.enum(["preserve", "remove"]).optional(),
151
- overwrite: z.boolean().optional()
152
- }
363
+ overwrite: z.boolean().optional(),
364
+ ...contentCheckInputs
365
+ },
366
+ outputSchema: workflowResultShape,
367
+ annotations: destructiveHint
153
368
  },
154
369
  async (options, extra) => {
155
370
  const notifications = mcpProgress(extra);
156
371
  const response = await safely(
157
- async () => normalize({
158
- input: options.input,
159
- output: options.output,
160
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
161
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
162
- ...options.audio === void 0 ? {} : { audio: options.audio },
163
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
164
- signal: extra.signal,
165
- onProgress: notifications.notify
166
- })
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
+ )
167
386
  );
168
387
  await notifications.drain();
169
388
  return response;
@@ -172,29 +391,36 @@ function createMcpServer() {
172
391
  server.registerTool(
173
392
  "extract_audio",
174
393
  {
175
- description: "Extract and verify audio from any media source. Reports MCP progress when requested.",
394
+ description: "Extract and verify audio from any media source. Reports MCP progress when requested. Overwrite is destructive.",
176
395
  inputSchema: {
177
396
  input: z.string().min(1),
178
397
  output: z.string().min(1),
179
398
  format: z.enum(["m4a", "mp3", "wav"]).optional(),
180
399
  trimStartSeconds: z.number().nonnegative().optional(),
181
400
  durationSeconds: z.number().positive().optional(),
182
- overwrite: z.boolean().optional()
183
- }
401
+ overwrite: z.boolean().optional(),
402
+ ...contentCheckInputs
403
+ },
404
+ outputSchema: workflowResultShape,
405
+ annotations: destructiveHint
184
406
  },
185
407
  async (options, extra) => {
186
408
  const notifications = mcpProgress(extra);
187
409
  const response = await safely(
188
- async () => extractAudio({
189
- input: options.input,
190
- output: options.output,
191
- ...options.format === void 0 ? {} : { format: options.format },
192
- ...options.trimStartSeconds === void 0 ? {} : { trimStartSeconds: options.trimStartSeconds },
193
- ...options.durationSeconds === void 0 ? {} : { durationSeconds: options.durationSeconds },
194
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
195
- signal: extra.signal,
196
- onProgress: notifications.notify
197
- })
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
+ )
198
424
  );
199
425
  await notifications.drain();
200
426
  return response;
@@ -203,27 +429,66 @@ function createMcpServer() {
203
429
  server.registerTool(
204
430
  "extract_frame",
205
431
  {
206
- description: "Extract and verify a still frame from a video source. Reports MCP progress when requested.",
432
+ description: "Extract and verify a still frame from a video source. The timestamp must be within the source duration. Reports MCP progress when requested. Overwrite is destructive.",
207
433
  inputSchema: {
208
434
  input: z.string().min(1),
209
435
  output: z.string().min(1),
210
436
  atSeconds: z.number().nonnegative().optional(),
211
437
  format: z.enum(["jpg", "png"]).optional(),
212
- overwrite: z.boolean().optional()
213
- }
438
+ overwrite: z.boolean().optional(),
439
+ ...contentCheckInputs
440
+ },
441
+ outputSchema: workflowResultShape,
442
+ annotations: destructiveHint
214
443
  },
215
444
  async (options, extra) => {
216
445
  const notifications = mcpProgress(extra);
217
446
  const response = await safely(
218
- async () => extractFrame({
219
- input: options.input,
220
- output: options.output,
221
- ...options.atSeconds === void 0 ? {} : { atSeconds: options.atSeconds },
222
- ...options.format === void 0 ? {} : { format: options.format },
223
- ...options.overwrite === void 0 ? {} : { overwrite: options.overwrite },
224
- signal: extra.signal,
225
- onProgress: notifications.notify
226
- })
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
+ )
460
+ );
461
+ await notifications.drain();
462
+ return response;
463
+ }
464
+ );
465
+ server.registerTool(
466
+ "concatenate_media",
467
+ {
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.",
469
+ inputSchema: {
470
+ inputs: z.array(z.string().min(1)).min(2).describe("Every clip to join, in playback order. The first entry leads the output."),
471
+ output: z.string().min(1),
472
+ overwrite: z.boolean().optional(),
473
+ ...contentCheckInputs
474
+ },
475
+ outputSchema: workflowResultShape,
476
+ annotations: destructiveHint
477
+ },
478
+ async (options, extra) => {
479
+ const notifications = mcpProgress(extra);
480
+ const response = await safely(
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
+ )
227
492
  );
228
493
  await notifications.drain();
229
494
  return response;
@@ -232,40 +497,112 @@ function createMcpServer() {
232
497
  server.registerTool(
233
498
  "execute_media_plan",
234
499
  {
235
- description: "Execute a serialized semantic Media IR plan.",
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.`,
236
501
  inputSchema: {
237
- plan: z.union([z.string().min(1), mediaPlanSchema]),
502
+ plan: z.union([z.string().min(1), planRefSchema]),
238
503
  output: z.string().min(1),
239
- overwrite: z.boolean().optional()
240
- }
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
514
+ },
515
+ annotations: destructiveHint
241
516
  },
242
- async ({ plan: input, output, overwrite }, extra) => {
517
+ async ({ plan: input, output, overwrite, writeReceipt, resume, contentChecks, warnOnly }, extra) => {
243
518
  const notifications = mcpProgress(extra);
244
519
  const response = await safely(async () => {
245
520
  const plan = normalizePlan(input);
246
521
  const execution = await executePlan(plan, {
247
522
  output,
248
523
  ...overwrite === void 0 ? {} : { overwrite },
524
+ ...writeReceipt === void 0 ? {} : { writeReceipt },
525
+ ...resume === void 0 ? {} : { resume },
526
+ ...contentCheckOptions(contentChecks),
527
+ ...warnOnly === void 0 ? {} : { warnOnly },
528
+ signal: extra.signal,
529
+ onProgress: notifications.notify
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
+ }
540
+ return {
541
+ output: execution.output,
542
+ ...execution.resumed === void 0 ? {} : { resumed: execution.resumed },
543
+ ...execution.receipt === void 0 ? {} : { receipt: execution.receipt },
544
+ verification
545
+ };
546
+ });
547
+ await notifications.drain();
548
+ return response;
549
+ }
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 },
249
573
  signal: extra.signal,
250
574
  onProgress: notifications.notify
251
575
  });
252
576
  return {
253
577
  output: execution.output,
254
- verification: verifyMedia(await inspectMedia(execution.output), plan.expectations)
578
+ resumed: execution.resumed === true,
579
+ ...execution.receipt === void 0 ? {} : { receipt: execution.receipt }
255
580
  };
256
581
  });
257
582
  await notifications.drain();
258
583
  return response;
259
584
  }
260
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
+ );
261
596
  server.registerTool(
262
597
  "verify_media",
263
598
  {
264
- description: "Verify output media against a serialized semantic Media IR plan.",
599
+ description: "Verify output media against a serialized semantic Media IR plan. Accepts a plan object or JSON string.",
265
600
  inputSchema: {
266
601
  output: z.string().min(1),
267
- plan: z.union([z.string().min(1), mediaPlanSchema])
268
- }
602
+ plan: z.union([z.string().min(1), planRefSchema])
603
+ },
604
+ outputSchema: verificationShape,
605
+ annotations: readOnlyHint
269
606
  },
270
607
  async ({ output, plan: input }) => safely(async () => {
271
608
  const plan = normalizePlan(input);
@@ -274,8 +611,17 @@ function createMcpServer() {
274
611
  );
275
612
  return server;
276
613
  }
614
+ function workflowResponse(result2) {
615
+ const rest = { ...result2 };
616
+ delete rest.serializedPlan;
617
+ return rest;
618
+ }
277
619
  function result(value) {
278
- 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);
279
625
  }
280
626
  async function safely(operation) {
281
627
  try {
@@ -285,8 +631,20 @@ async function safely(operation) {
285
631
  code: "UNEXPECTED_ERROR",
286
632
  message: error instanceof Error ? error.message : String(error)
287
633
  };
288
- 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));
289
646
  }
647
+ return sources;
290
648
  }
291
649
  function normalizePlan(input) {
292
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.1.1",
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.1.1",
43
- "@hadialmarzooq/agent-media-ffmpeg": "0.1.1"
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",