Skip to main content
Glama
index.ts7.85 kB
import "dotenv/config"; import fetch, { RequestInit } 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!; interface RunwayTask { id: string; status: | "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "CANCELLED" | "THROTTLED"; url?: string; error?: string; [key: string]: any; // for other task-specific fields } const server = new McpServer({ name: "Runway", version: "1.0.0" }); async function callRunway( path: string, opts: Partial<RequestInit> = {} ): Promise<unknown> { const res = await fetch(`${API_BASE}${path}`, { ...opts, headers: { Authorization: `Bearer ${SECRET}`, "X-Runway-Version": RUNWAY_VERSION, "Content-Type": "application/json", ...(opts.headers || {}), }, } as RequestInit); if (!res.ok) throw new Error(`Runway ${res.status}: ${await res.text()}`); return res.json(); } async function waitForTaskCompletion(taskId: string): Promise<RunwayTask> { while (true) { const task = (await callRunway(`/tasks/${taskId}`)) as RunwayTask; if ( task.status === "SUCCEEDED" || task.status === "FAILED" || task.status === "CANCELLED" ) { return task; } // Wait 5 seconds before next poll await new Promise((resolve) => setTimeout(resolve, 5_000)); } } async function callRunwayAsync( path: string, opts: Partial<RequestInit> = {} ): Promise<RunwayTask> { const response = (await callRunway(path, opts)) as { id?: string; } & RunwayTask; // 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); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/runwayml/runway-api-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server