@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.
- package/dist/index.js +450 -92
- 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
|
-
|
|
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
|
-
{
|
|
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:
|
|
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 () =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 () =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 () =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 () =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 () =>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
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),
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
43
|
-
"@hadialmarzooq/agent-media-ffmpeg": "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",
|