@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.
- package/dist/index.js +399 -84
- 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.
|
|
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:
|
|
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 () =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 () =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 () =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 () =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 () =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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
|
-
|
|
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 () =>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
43
|
-
"@hadialmarzooq/agent-media-ffmpeg": "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",
|