Skip to content
1 change: 1 addition & 0 deletions src/sdk/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./listRenderableItems";
export * from "./getRenderableItemDetails";
export * from "./renderItem";
48 changes: 48 additions & 0 deletions src/sdk/api/renderItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AxiosResponse } from "axios";
import { createAxiosInstance } from "../../axiosConfig";
import { DesignRenderDto, ProjectRenderDto, Render } from "../types";

const api = createAxiosInstance();

type RenderParams = {
isDesign: boolean;
projectDesignId: string;
templateVariantId: string;
parameters?: { [key: string]: any };
};

export const renderItem = async (params: RenderParams): Promise<Render> => {
if (params.isDesign) {
return await renderDesign(params);
} else {
return await renderProject(params);
}
Comment thread
danixeee marked this conversation as resolved.
Outdated
};

const renderDesign = async (params: RenderParams): Promise<Render> => {
const response = await api.post<
Render,
AxiosResponse<Render>,
DesignRenderDto
>("/api/v2/designs", {
designId: params.projectDesignId,
variantId: params.templateVariantId,
parameters: params.parameters,
});

return response.data;
};

const renderProject = async (params: RenderParams): Promise<Render> => {
const response = await api.post<
Render,
AxiosResponse<Render>,
ProjectRenderDto
>("/api/v2/renders", {
projectId: params.projectDesignId,
templateId: params.templateVariantId,
parameters: params.parameters,
Comment thread
danixeee marked this conversation as resolved.
});

return response.data;
};
37 changes: 37 additions & 0 deletions src/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,40 @@ export type RenderableItemDetails = {
defaultValue?: any | null;
}[];
};

enum RenderState {
PENDING = "PENDING",
THROTTLED = "THROTTLED",
QUEUED = "QUEUED",
IN_PROGRESS = "IN_PROGRESS",
DONE = "DONE",
FAILED = "FAILED",
INVALID = "INVALID",
CANCELLED = "CANCELLED",
}

export type Render = {
id: string;
projectId: string;
templateId: string;
expirationDate?: string;
state: RenderState;
output?: string;
projectName: string;
templateName: string;
error: { [key: string]: string | object };
};

export type AbstractRenderDto = {
parameters?: { [key: string]: any };
};

export type ProjectRenderDto = AbstractRenderDto & {
projectId: string;
templateId: string;
};

export type DesignRenderDto = AbstractRenderDto & {
Comment thread
danixeee marked this conversation as resolved.
Outdated
designId: string;
variantId: string;
};
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import {
registerListRenderableItems,
registerGetRenderableItemDetails,
registerRenderItem,
} from "./tools";

export class PlainlyMcpServer {
Expand All @@ -19,6 +20,7 @@ export class PlainlyMcpServer {
// Register tools
registerListRenderableItems(this.server);
registerGetRenderableItemDetails(this.server);
registerRenderItem(this.server);
}

async start() {
Expand Down
1 change: 1 addition & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./getRenderableItemDetails";
export * from "./listRenderableItems";
export * from "./renderItem";
204 changes: 204 additions & 0 deletions src/tools/renderItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getRenderableItemsDetails, renderItem } from "../sdk";

export function registerRenderItem(server: McpServer) {
const Input = {
isDesign: z
.boolean()
.describe(
"True when the parent is a Design; false when it is a Project."
),
projectDesignId: z
.string()
.describe("Parent identifier (projectId or designId)."),
templateVariantId: z
.string()
Comment thread
danixeee marked this conversation as resolved.
.describe(
"Template/variant identifier (the renderable leaf under the parent)."
),
parameters: z
.record(z.any())
.default({})
.describe(
"Key-value parameters required by the chosen template/variant to customize the render. Mandatory parameters must be provided. Parameter type must be respected."
),
};

const Output = {
Comment thread
danixeee marked this conversation as resolved.
id: z.string().optional().describe("Server-assigned render job ID."),
state: z
.enum([
Comment thread
danixeee marked this conversation as resolved.
Outdated
"PENDING",
"THROTTLED",
"QUEUED",
"IN_PROGRESS",
"DONE",
"FAILED",
"INVALID",
"CANCELLED",
])
.optional()
.describe("Current state of the render job."),
output: z
.string()
.nullable()
.optional()
.describe("URL to the rendered video, if state is DONE."),
error: z
.record(z.any())
.nullable()
.optional()
.describe("Error details, if state is FAILED or INVALID."),
};

server.registerTool(
"render_item",
{
title: "Render Item",
description: `
Create a render for a selected Project template or Design variant with specified parameters.

How to use:
- Call this after the user selects a candidate from \`get_renderable_items_details\`.
- Call this only once the user approved all parameters for the chosen template/variant.

Guidance:
- Use parameters to customize the render.
- All mandatory parameters must be provided.
- Provide values for optional parameters if it makes sense.
- Parameter types must be respected:
- STRING: text string relevant to the parameter context.
- MEDIA: URL to a media file (image, audio, or video). Ensure the URL is publicly accessible and points directly to the media file.
- MEDIA (image): URL to an image file (jpg, png, etc.).
- MEDIA (audio): URL to an audio file (mp3, wav, etc.).
- MEDIA (video): URL to a video file (mp4, mov, etc.).
- COLOR: hex color code (e.g. #FF5733).
- If a parameter has a default value and the user does not provide a value, the default will be used.
- If the user is unsure about a parameter, ask for clarification rather than guessing.
- When referencing parameters in conversation, use their \`label\` or \`description\` for clarity.

Use when:
- The user wants to create a video from a specific template/variant with defined parameters.
`,
inputSchema: Input,
outputSchema: Output,
},
async ({ isDesign, projectDesignId, templateVariantId, parameters }) => {
// TODO: Handle object parameters "my.parameter.x"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess not needed, if explained how "keyed" params should be represented in the parameters object (just add instruction point)..


try {
// Validate that the chosen project / design exists
const projectDesignItems = await getRenderableItemsDetails(
projectDesignId,
isDesign
);

if (projectDesignItems.length === 0) {
return {
content: [
{
type: "text",
text: `Could not find a ${
isDesign ? "design" : "project"
} with id ${projectDesignId} .`,
},
],
};
}

// Validate that the chosen template / variant exists
const renderableItem = projectDesignItems.find(
(item) => item.templateVariantId === templateVariantId
);

if (!renderableItem) {
return {
content: [
{
type: "text",
text: `Could not find a ${
isDesign ? "variant" : "template"
} with id ${templateVariantId} under the specified ${
isDesign ? "design" : "project"
} (${projectDesignId}).`,
},
],
structuredContent: {},
isError: true,
};
}

// Validate parameters
const mandatoryParams = renderableItem.parameters.filter(
(p) => p.mandatory
);
const providedParams = Object.keys(parameters);
const missingParams = mandatoryParams.filter(
(p) => !providedParams.includes(p.key)
);

if (missingParams.length > 0) {
return {
content: [
{
type: "text",
text: `Missing required parameters: ${missingParams
.map((p) => p.key)
.join(", ")}.`,
},
],
structuredContent: {},
isError: true,
};
}

// If everything looks good, submit the render
const render = await renderItem({
isDesign,
projectDesignId,
templateVariantId,
parameters,
});

return {
content: [
Copy link
Copy Markdown
Contributor

@ivansenic ivansenic Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sence that we send also a https://modelcontextprotocol.io/specification/2025-06-18/server/tools#resource-links for this render html page, so link to app.plainlyvideos.com/renderid imo this makes a lot of sense, since I could at least get link until we have pooling

content is array and you can send multiple things

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I tried but it is not really working like that, see above. But I can include a link in the output

{
Comment thread
danixeee marked this conversation as resolved.
type: "text",
text: `🚀 Render submitted successfully!

**Render ID:** ${render.id}
**Parameters Used:**
${Object.entries(parameters)
.map(([key, value]) => {
const param = renderableItem.parameters.find((p) => p.key === key);
return `• ${param?.label || key}: ${value}`;
})
.join("\n")}

The render is being processed. Use the render ID to check status and retrieve the final video when complete.`,
},
],
structuredContent: {
id: render.id,
state: render.state ?? "QUEUED",
output: render.output ?? null,
error: null,
},
};
} catch (err: any) {
Comment thread
danixeee marked this conversation as resolved.
// Handle API errors gracefully
return {
content: [
{
type: "text",
text: `Failed to create render: ${err}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really good? would add error in structured content.. Seems also that validation passed, but call failed? here he is using wrong parameter name 9.99 -> you did not exclude static params from the template

image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus he does not understand it's not good

image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a custom error with solutions for each. I have no idea if this works or not with LLM, lets test it a bit. The idea is to give LLM instructions what to do in case of any known error happens. Wdyt?

image

},
],
structuredContent: {},
Comment thread
danixeee marked this conversation as resolved.
Outdated
isError: true,
};
}
}
);
}