runway-api-mcp-server
Version:
MCP server for Runway API - Generate videos and images using Runway's AI models
159 lines (158 loc) • 7.55 kB
JavaScript
#!/usr/bin/env node
import "dotenv/config";
import fetch from "node-fetch";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const API_BASE = "https://api.dev.runwayml.com/v1";
const RUNWAY_VERSION = "2024-11-06";
const SECRET = process.env.RUNWAYML_API_SECRET;
const server = new McpServer({ name: "Runway", version: "1.0.0" });
async function callRunway(path, opts = {}) {
const res = await fetch(`${API_BASE}${path}`, {
...opts,
headers: {
Authorization: `Bearer ${SECRET}`,
"X-Runway-Version": RUNWAY_VERSION,
"Content-Type": "application/json",
...(opts.headers || {}),
},
});
if (!res.ok)
throw new Error(`Runway ${res.status}: ${await res.text()}`);
return res.json();
}
async function waitForTaskCompletion(taskId) {
while (true) {
const task = (await callRunway(`/tasks/${taskId}`));
if (task.status === "SUCCEEDED" ||
task.status === "FAILED" ||
task.status === "CANCELLED") {
return task;
}
// Wait 5 seconds before next poll
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
async function callRunwayAsync(path, opts = {}) {
const response = (await callRunway(path, opts));
// If the response has a taskId, wait for completion
if (response?.id) {
return waitForTaskCompletion(response.id);
}
// If no taskId, just return the response as is
return response;
}
// 1. Generate video from image
server.tool("runway_generateVideo", "Generate a video from an image and a text prompt. Accepted ratios are 1280:720, 720:1280, 1104:832, 832:1104, 960:960, 1584:672. Use 1280:720 by default. For duration, there are only either 5 or 10 seconds. Use 5 seconds by default. If the user asks to generate a video, always first use generateImage to generate an image first, then use the image to generate a video.", {
promptImage: z.string(),
promptText: z.string().optional(),
ratio: z.string(),
duration: z.number(),
}, async (params) => {
const task = await callRunwayAsync("/image_to_video", {
method: "POST",
body: JSON.stringify({
model: "gen4_turbo",
promptImage: params.promptImage,
promptText: params.promptText,
ratio: params.ratio,
duration: params.duration,
}),
});
return { content: [{ type: "text", text: JSON.stringify(task) }] };
});
// 2. Generate image from text
server.tool("runway_generateImage", `Generate an image from a text prompt and optional reference images. Available ratios are 1920:1080, 1080:1920, 1024:1024,
1360:768, 1080:1080, 1168:880, 1440:1080, 1080:1440, 1808:768, 2112:912, 1280:720, 720:1280,
720:720, 960:720, 720:960, 1680:720. Use 1920:1080 by default. It also accepts reference images, in the form of either a url or a base64 encoded image.
Each reference image has a tag, which is a string that refers to the image from the user prompt. For example, if the user prompt is "IMG_1 on a red background",
and the reference image has the tag "IMG_1", the model will use that reference image to generate the image. The return of this function will contain a url to the generated image.`, {
promptText: z.string(),
ratio: z.string(),
referenceImages: z
.array(z.object({ uri: z.string(), tag: z.string().optional() }))
.optional(),
}, async ({ promptText, ratio, referenceImages }) => {
const task = await callRunwayAsync("/text_to_image", {
method: "POST",
body: JSON.stringify({
model: "gen4_image",
promptText,
ratio,
referenceImages,
}),
});
if (task.status === "SUCCEEDED") {
return {
content: [
{
type: "text",
text: `Here is the URL of the image: ${task.output[0]}. Return to the user, as a markdown link, the URL of the image and the prompt that was used to generate the image.`,
},
],
};
}
else {
return { content: [{ type: "text", text: JSON.stringify(task) }] };
}
});
// 3. Upscale a video
server.tool("runway_upscaleVideo", "Upscale a video to a higher resolution. videoUri takes in a url of a video or a data uri of a video.", { videoUri: z.string() }, async ({ videoUri }) => {
const task = await callRunwayAsync("/video_upscale", {
method: "POST",
body: JSON.stringify({ videoUri, model: "upscale_v1" }),
});
return { content: [{ type: "text", text: JSON.stringify(task) }] };
});
// 4. Edit a video
server.tool("runway_editVideo", `Edit a video using Runway Aleph. promptText is a prompt for the video. videoUri takes in a url of a video or a data uri of a video. Accepted Ratio values are 1280:720, 720:1280, 1104:832, 960:960, 832:1104, 1584:672, 848:480, 640:480. Use 1280:720 by default. It also accepts reference images, in the form of either a url or a base64 encoded image.
Each reference image has a tag, which is a string that refers to the image from the user prompt. For example, if the user prompt is "IMG_1 on a red background",
and the reference image has the tag "IMG_1", the model will use that reference image to generate the image.`, {
promptText: z.string(),
videoUri: z.string(),
ratio: z.string(),
referenceImages: z
.array(z.object({ uri: z.string(), tag: z.string().optional() }))
.optional(),
}, async ({ promptText, videoUri, ratio, referenceImages }) => {
const task = await callRunwayAsync("/video_to_video", {
method: "POST",
body: JSON.stringify({
promptText,
videoUri,
ratio,
...(referenceImages && referenceImages.length > 0
? { references: referenceImages }
: {}),
model: "gen4_aleph",
}),
});
return { content: [{ type: "text", text: JSON.stringify(task) }] };
});
// 5. Get task detail
server.tool("runway_getTask", "Get the details of a task, if the task status is 'SUCCEEDED', there will be a 'url' field in the response. If the task status is 'FAILED', there will be a 'error' field in the response. If the task status is 'PENDING' or 'RUNNING', you can call this tool again in 5 seconds to get the task details.", {
taskId: z.string(),
}, async ({ taskId }) => {
const task = await callRunway(`/tasks/${taskId}`);
return { content: [{ type: "text", text: JSON.stringify(task) }] };
});
// 6. Cancel/delete a task
server.tool("runway_cancelTask", "Deletes or cancels a given task.", { taskId: z.string() }, async ({ taskId }) => {
await callRunway(`/tasks/${taskId}`, { method: "DELETE" });
return { content: [{ type: "text", text: `Task ${taskId} cancelled.` }] };
});
// 7. Get organization info
server.tool("runway_getOrg", "Returns details like credit balance, usage details, and organization information.", {}, async () => {
const org = await callRunway("/organization");
return { content: [{ type: "text", text: JSON.stringify(org) }] };
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Runway MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});