Skip to main content
Glama
kenari-media-mcp

kenari-media-mcp

by dnshtm9

kenari-media-mcp

An MCP (Model Context Protocol) server for the Kenari.id media APIs (https://kenari.id/v1). It gives any MCP client (Claude Desktop, Cursor, OpenCode, etc.) 8 tools for image generation and editing plus video generation, extension, status polling, and download. It runs over stdio and is designed for agentic use: all generated media is written to disk under a configurable output directory and returned as absolute file paths — never base64 or ImageContent — so the calling agent can open, move, or post-process files directly.

This project fills a gap: Kenari's own official MCP server covers docs, balance, and search, but not the media generation endpoints. This is a third-party, unaffiliated implementation for those endpoints.

⚠️ Cost warning — real money

Generation endpoints spend real Indonesian Rupiah (IDR) per call, billed to your Kenari account. Prices vary per model — gpt-image-2 is roughly 175 IDR per image, while others cost several times more. Treat the catalog prices you see via list_media_models as examples only; live prices come from list_media_models.

Note that preview does not save money — files are always written to disk regardless.

Never point automated tests or scheduled jobs at generation tools; the test suite uses a mocked fetch and spends nothing.

Related MCP server: Magic Hour MCP Server

Requirements

  • Node.js >= 20 (engines field is enforced; node --version to check)

  • pnpm (or npm) for installing and running scripts

  • A Kenari API key (kn-...), obtained from the Kenari dashboard. Required for everything except list_media_models (GET /v1/models is public).

Install from source

This is the primary installation path until the package is published to npm (it is not on npm yet — npx will not work yet).

git clone https://github.com/dnshtm9/kenari-media-mcp.git
cd kenari-media-mcp
pnpm install
pnpm build

The build writes the server entry to dist/index.js (the bin is kenari-media-mcp).

Environment variables

Var

Default

Notes

KENARI_API_KEY

(none)

Required for everything except list_media_models.

KENARI_BASE_URL

https://kenari.id/v1

Trailing slashes trimmed.

KENARI_OUTPUT_DIR

./kenari-output

Media save directory (created recursively). Absolute paths are returned.

KENARI_ALLOW_VIDEO

allow

Set to 0/false to disable all video tools with a clear error.

KENARI_MAX_IMAGE_N

4

Preflight cap for n.

KENARI_MAX_VIDEO_DURATION

15

Preflight cap for duration (seconds).

KENARI_MAX_COST_IDR_PER_CALL

(none)

Optional per-call IDR ceiling for image and video tools (best-effort catalog lookup; fail-open when the price is unknown).

.env files — important caveat

The repository includes a .env.example. Copy it to .env if you want pnpm start (or other local runs) to pick up defaults — pnpm start loads .env via a small built-in reader; existing environment variables always win.

MCP clients do not reliably load .env. When a client spawns this server, the server's working directory is often not the repository, so any .env next to it may be invisible. You must inject environment variables in the client JSON env block. Treat .env as a local-run convenience only.

Never commit .env or any file containing your API key.

MCP client configuration

Point the client at the built server. Most MCP clients require an absolute path in args — relative paths are resolved against the client's own working directory, not yours.

Substitute the location where you cloned the repository. On Windows, double every backslash in JSON strings and save the config file as UTF-8 without BOM (e.g. VS Code → "Save with Encoding → UTF-8"); a BOM can break config parsing.

Windows pattern:

{
  "mcpServers": {
    "kenari-media": {
      "command": "node",
      "args": ["C:\\\\path\\\\to\\\\kenari-media-mcp\\\\dist\\\\index.js"],
      "env": {
        "KENARI_API_KEY": "kn-REPLACE_ME",
        "KENARI_OUTPUT_DIR": "C:\\\\path\\\\to\\\\kenari-output"
      }
    }
  }
}

macOS / Linux pattern:

{
  "mcpServers": {
    "kenari-media": {
      "command": "node",
      "args": ["/path/to/kenari-media-mcp/dist/index.js"],
      "env": {
        "KENARI_API_KEY": "kn-REPLACE_ME",
        "KENARI_OUTPUT_DIR": "/path/to/kenari-output"
      }
    }
  }
}

KENARI_OUTPUT_DIR is any directory you choose; if you omit it, media is saved to ./kenari-output relative to the server's working directory (which may not be where you expect — setting it explicitly is safer).

After npm publish: npx (not yet possible)

The package is not published to npm yet, so the following does not work today. Once it is published, npx-based config will be:

Unix / macOS:

{
  "mcpServers": {
    "kenari-media": {
      "command": "npx",
      "args": ["-y", "kenari-media-mcp"],
      "env": {
        "KENARI_API_KEY": "kn-REPLACE_ME"
      }
    }
  }
}

Windows (npx is npx.cmd, which cannot be spawned directly, so clients must go through cmd):

{
  "mcpServers": {
    "kenari-media": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "kenari-media-mcp"],
      "env": {
        "KENARI_API_KEY": "kn-REPLACE_ME"
      }
    }
  }
}

Until then, use the local node + absolute-path config above.

Tools (8)

  1. list_media_models { modality?: "image"|"video" } — public catalog with per-image IDR costs. Snapshot example: 5 image models, 0 video-generation models (the video list may be [] — video tools are still implemented and pass through to the API).

  2. generate_image { model, prompt, n?, size?, background?: transparent|opaque|auto, preview?=false } — text-to-image; n capped by KENARI_MAX_IMAGE_N.

  3. edit_image { model, prompt, image_path, mask_path?, n?, size?, background?: opaque|auto } — reads local image files you pass; background: transparent is rejected by schema.

  4. create_video { model, prompt, duration?, resolution?, image_url?, end_image_url?, input_images?, video_url?, aspect_ratio? } — text/image-to-video; duration capped by KENARI_MAX_VIDEO_DURATION; also honors KENARI_MAX_COST_IDR_PER_CALL (per-call preflight estimate, see "Cost caps" below).

  5. extend_video { model, source, prompt?, duration? } — source is a job id or an https URL; local file paths are rejected before any HTTP request. Also honors KENARI_MAX_COST_IDR_PER_CALL like create_video.

  6. get_video_status { id } — checks a video job; auto-downloads the result when done.

  7. wait_for_video { id, timeout_ms?=1200000, download?=true } — polls every 5s with backoff capped at 15s; emits notifications/progress when the client sends a progress token. On timeout it returns still_rendering with isError: false so the agent can retry later.

  8. download_video { id } — downloads a finished video; returns a still_rendering error while rendering.

Error codes, heartbeat, timeouts

Error codes surfaced in structuredContent: unauthorized | insufficient_balance | content_policy_violation | rate_limited | invalid_model | video_failed | video_expired | still_rendering | bad_request | upstream_error.

  • 401 error bodies are plain text and are never assumed to be JSON.

  • Kenari image POSTs may stream ASCII-space "heartbeat" characters (~every 20s) before the JSON body — the server reads the full body, trims, then parses.

  • HTTP timeout: 300s for image calls. wait_for_video default budget: 20 minutes.

  • stdout carries JSON-RPC only; all logs go to stderr (console.error).

Cost caps — per-call, fail-open, no accumulation

KENARI_MAX_COST_IDR_PER_CALL applies to generate_image, edit_image, create_video, and extend_video as a per-call preflight estimate, checked against the live catalog (list_media_models pricing) before any paid POST is made. Three fail-open paths, deliberately identical for images and video:

  1. the env var is unset,

  2. the catalog lookup itself fails, or

  3. the model has no parseable price line.

In those cases the call proceeds and a warning is written to stderr. Video pricing calibration: no video-generation model has appeared in Kenari's catalog yet (pricing_lines with billable: "output_video" and a second/video/call unit). Once one appears, per-second prices are multiplied by duration (falling back to KENARI_MAX_VIDEO_DURATION when unset) and flat per-call prices are used as-is — no code change expected, but re-verify the estimate against the live catalog then.

There is no cross-call accounting: nothing accumulates spend across calls or sessions. An in-memory ledger was considered and intentionally deferred — it would reset on every server restart, producing dishonest numbers. If you need a hard budget, set the cap and monitor your Kenari dashboard.

Manual live smoke test

⚠️ This spends real money. Do it once by hand, never in CI or automated tests.

  1. Build (pnpm build) and set KENARI_API_KEY.

  2. Call list_media_models → expect the image model catalog.

  3. Call generate_image with the cheapest model, n: 1 → confirm a file appears in the output dir.

  4. Call get_video_status on a bogus id → expect video_expired/bad_request, no spend.

  5. Only then try create_video if a video model exists in the catalog.

Development

pnpm test       # tsx --test tests/**/*.test.ts — mocked fetch, no live spend
pnpm typecheck  # tsc --noEmit
pnpm lint       # tsc --noEmit

Tests mock fetch entirely, so running them costs nothing and requires no API key. The model-catalog fixture lives at tests/fixtures/models.json (a copy of a real GET /v1/models snapshot).

Security notes

  • kn-* API key values are redacted in logs.

  • The Authorization: Bearer header is attached only to requests whose host matches KENARI_BASE_URL's host (exact host match, port-agnostic, so self-hosted bases keep working). CDN URLs returned by the API (content/image downloads) are fetched unauthenticated, and a CDN-side 401 is reported as upstream_error — never as an API-key failure.

  • Never commit API keys (.env, shell history, config dumps).

  • edit_image reads local files you pass to it — only pass files you intend to upload. Obvious sensitive files are refused before reading: .env / *.env (incl. .env.*), private keys (.pem, .key, id_rsa/id_ed25519-style names), and paths under .ssh, .aws, or .gcloud directories. The blocklist is a guardrail, not a sandbox — do not point the tool at directories you do not control.

  • Generation costs money on every call; beware of an agent looping on generation tools and burning IDR unattended (see "Cost caps" above).

License

See LICENSE.

Available Tools

8 tools
create_videoCreate VideoA

Start a Kenari video job via POST /v1/videos/generations. Returns job id; poll with get_video_status/wait_for_video. Costs money.

Inputs: model (required), prompt (required), duration, image_url, +5 more. Returns: id, op, raw, code, +4 more

download_videoDownload VideoA

Download finished video bytes via GET /v1/videos/{id}/content to KENARI_OUTPUT_DIR. Errors with still_rendering while the job renders.

Inputs: id (required). Returns: id, op, code, hint, +3 more

edit_imageEdit ImageA

Edit a local image via POST /v1/images/edits (multipart: image, mask?, prompt, model, n?, size?, background?). background=transparent is rejected (use generate_image instead).

Inputs: model (required), prompt (required), image_path (required), n, +3 more. Returns: id, op, code, hint, +5 more

extend_videoExtend VideoA

Extend a finished video via POST /v1/videos/extensions {model, video:{url}, prompt?, duration?}. source is a job id or https URL — local paths are rejected before any HTTP call.

Inputs: model (required), source (required), prompt, duration. Returns: id, op, raw, code, +4 more

generate_imageGenerate ImageA

Generate image(s) via POST /v1/images/generations. Saves PNG/JPG to KENARI_OUTPUT_DIR and returns absolute path(s). Costs IDR per image — check list_media_models first.

Inputs: model (required), prompt (required), n, size, +2 more. Returns: id, op, code, hint, +5 more

get_video_statusGet Video StatusA
Idempotent

GET /v1/videos/{id}. Reports queued/rendering/done/failed/expired. Auto-downloads to KENARI_OUTPUT_DIR when done.

Inputs: id (required). Returns: id, op, code, hint, +3 more

list_media_modelsList Kenari Media ModelsA
Read-only

List Kenari media-capable models (image/video). Public, no key needed.

Inputs: modality. Returns: id, op, code, hint, +3 more

wait_for_videoWait For VideoA

Poll GET /v1/videos/{id} every 5s (backoff cap 15s) until done/failed/expired or timeout (default 20 min). Auto-downloads when done. Timeout returns still_rendering with isError=false.

Inputs: id (required), download, timeout_ms. Returns: id, op, code, hint, +3 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 8 tools

    Disambiguation4/5

    Image generation/edit and video create/extend are clearly separated, and each tool has a distinct action. However, get_video_status, wait_for_video, and download_video overlap somewhat: both status tools auto-download and wait_for_video is essentially status polling with a loop, which can cause agent misselection.

    Naming Consistency5/5

    All tool names follow a consistent snake_case verb_noun pattern: generate_image, edit_image, create_video, extend_video, download_video, etc. The naming style is uniform and predictable across image and video workflows.

    Tool Count5/5

    Eight tools is well-scoped for a media generation server covering both images and videos. Each tool maps to a meaningful API operation or workflow helper without unnecessary redundancy.

    Completeness4/5

    The core media lifecycle is covered: list models, generate/edit images, create/extend videos, check status, wait, and download results. Minor gaps exist around video cancellation/deletion and explicit image result management, but agents can complete primary workflows without workarounds.

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers