Skip to main content
This page shows how to expose Lavendly tools through Claude’s tool_use mechanism when you’re building your own agent runtime, without going through the MCP transport. When to use this:
  • You’re hosting your own agent and want full control over the prompting and the loop.
  • You’re embedding Lavendly inside a larger product where the MCP stdio model doesn’t fit (serverless, edge, etc.).
  • You want to combine Lavendly tools with tools from other systems in one tool call surface.

The pattern

1

Fetch the Lavendly operation catalog

GET /v1/_schema returns every operation with its input shape. Convert that into Claude’s tool definition format once at startup.
2

Load a skill into your system prompt

Concatenate one or more skill bodies (storyteller, multi-clip, cost-aware) into the system prompt. This is what makes the agent use the tools well.
3

Run the tool-use loop

On each tool_use block from Claude, call the matching Lavendly HTTP endpoint. Return the result as tool_result. Repeat until Claude stops with end_turn.

Reference implementation (Node)

import Anthropic from "@anthropic-ai/sdk";
import { Lavendly } from "@lavendly/sdk";
import { loadSkill } from "@lavendly/skills";

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const vs        = new Lavendly({ apiKey: process.env.LAVENDLY_API_KEY });

// 1. Build tools from the live operation catalog
const schema = await vs.introspect.schema();
const tools  = Object.entries(schema.operations).map(([name, op]) => ({
  name,
  description: op.description ?? `${op.method} ${op.path}`,
  input_schema: op.input_schema,
}));

// 2. Compose the system prompt with the storyteller skill
const skill = await loadSkill("storyteller");
const system = `You are a video producer. Use the Lavendly tools to
fulfill the user's creative briefs.

${skill}`;

// 3. Run the loop
async function runAgent(userBrief) {
  const messages = [{ role: "user", content: userBrief }];

  for (;;) {
    const response = await anthropic.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 4096,
      system,
      tools,
      messages,
    });

    messages.push({ role: "assistant", content: response.content });

    if (response.stop_reason !== "tool_use") {
      return response.content.find((c) => c.type === "text")?.text;
    }

    const toolResults = await Promise.all(
      response.content
        .filter((c) => c.type === "tool_use")
        .map(async (call) => ({
          type: "tool_result",
          tool_use_id: call.id,
          content: JSON.stringify(await vs.call(call.name, call.input)),
        })),
    );

    messages.push({ role: "user", content: toolResults });
  }
}

console.log(await runAgent("Make me a 5-second video of a fox in a bookshop."));
vs.call(name, args) is a thin dispatcher provided by the SDK that forwards to the right HTTP endpoint based on the operation name. It honors idempotency keys passed in args.idempotency_key.

Reference implementation (Python)

import os, json
from anthropic import Anthropic
from lavendly import Lavendly
from lavendly.skills import load_skill

anthropic = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
vs        = Lavendly(api_key=os.environ["LAVENDLY_API_KEY"])

schema = vs.introspect.schema()
tools  = [
    {
        "name": name,
        "description": op.get("description") or f"{op['method']} {op['path']}",
        "input_schema": op["input_schema"],
    }
    for name, op in schema["operations"].items()
]

system = f"""You are a video producer. Use the Lavendly tools to
fulfill the user's creative briefs.

{load_skill("storyteller")}"""

def run_agent(user_brief):
    messages = [{"role": "user", "content": user_brief}]
    while True:
        resp = anthropic.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            system=system,
            tools=tools,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason != "tool_use":
            return next((b.text for b in resp.content if b.type == "text"), None)

        tool_results = []
        for block in resp.content:
            if block.type == "tool_use":
                result = vs.call(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                })
        messages.append({"role": "user", "content": tool_results})

print(run_agent("Make me a 5-second video of a fox in a bookshop."))

Composing skills

To layer multiple skills (storyteller + cost-aware), concatenate them in priority order:
const system = [
  baseSystem,
  await loadSkill("cost-aware"),    // 'priority: high', comes first
  await loadSkill("storyteller"),   // domain skill
].join("\n\n");
The agent reads them in order and applies the high-priority constraints over the domain choices.

Why bother with the SDK pattern vs MCP?

MCPAnthropic SDK pattern
SetupAdd one entry to a config fileCode
Tool transportstdio JSON-RPCIn-process HTTP
CustomizationLimited, clients dictate the promptFull, you own the system prompt
Multi-tool compositionOne MCP per server, runtime mergesAll tools in one catalog
LatencySubprocess per callIn-process
Best forEnd users in Claude Desktop, Cursor, Claude CodeEngineers building their own agent product
If you ever need both, say, internal tooling via the SDK pattern plus end users via MCP, the same skill file works in either case.