## TypeScript client

### client.ts

```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"

const transport = new StreamableHTTPClientTransport(
  new URL(`${process.env.MCP_BASE_URL}/mcp/`),
  { requestInit: { headers: { Authorization: `Bearer ${process.env.MCP_JWT}` } } },
)

const client = new Client({ name: "wm-studio", version: "1.0.0" }, { capabilities: {} })
await client.connect(transport)

const tools = await client.listTools()
console.log(tools.tools.map((t) => t.name))

const result = await client.callTool({
  name: "studio_generate_image",
  arguments: {
    prompt: "minimalist product photo of a black ceramic mug",
    model: "fal-ai/flux-2-pro",
    aspect_ratio: "4:3",
  },
})
console.log(result)
```

## Python client

### client.py

```python
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

import os

def main():
    url = f"{os.environ['MCP_BASE_URL']}/mcp/"
    headers = {"Authorization": f"Bearer {os.environ['MCP_JWT']}"}
    async with streamablehttp_client(url, headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])
            result = await session.call_tool("studio_generate_image", {
                "prompt": "minimalist product photo of a black ceramic mug",
                "model": "fal-ai/flux-2-pro",
            })
            print(result)

import asyncio
asyncio.run(main())
```

## Raw curl

### Plain JSON-RPC over Streamable HTTP. Useful for smoke tests in CI.

```bash
curl -X POST "$MCP_BASE_URL/mcp/" \
  -H "Authorization: Bearer $MCP_JWT" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "studio_upscale_image",
      "arguments": {
        "image_url": "https://cdn.wm.studio/uploads/portrait.jpg",
        "scale": 4
      }
    }
  }'
```

## Async polling

### Long-running tools (`studio_generate_video`, `studio_digital_twin`,`studio_convert_to_3d`) return a `job_id` immediately. Poll until`status` is `completed` or `failed`.

```typescript
async function generateVideoAndWait(prompt: string, imageUrl: string) {
  const start = await client.callTool({
    name: "studio_generate_video",
    arguments: {
      prompt,
      image_url: imageUrl,
      model: "fal-ai/kling-video/v2.5-turbo/pro/image-to-video",
    },
  }) as { job_id: string }

for (let i = 0; i < 120; i++) {
    const status = await client.callTool({
      name: "studio_job_status",
      arguments: { job_id: start.job_id },
    }) as { status: string; video?: { url: string } }

if (status.status === "completed") return status.video?.url
    if (status.status === "failed") throw new Error("video render failed")
    await new Promise((r) => setTimeout(r, 5000))
  }
  throw new Error("timed out")
}
```
