dsh-media-mcp
# dsh-media-mcp
[中文](README.zh.md) | English
Turn a sentence into an image, a short video or spoken audio on your machine.
`dsh-media-mcp` is a small [MCP](https://modelcontextprotocol.io) server. Your AI client calls it,
it calls your media provider, and the result lands on disk as a normal file you can open. It also
exposes its own settings as tools, so changing the model, the size or the output folder is something
you say in chat — no settings screen to hunt for.
Works with any MCP client. Built for and tested with
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`).
## What you can do
Just ask:
- **"Draw a corgi surfing at sunset."** — you get back an absolute path to a saved JPEG.
- **"Make a five second clip of a paper boat drifting down a rainy street."** — a video, in a few
minutes (three to four, measured).
- **"Read this paragraph aloud."** — an MP3 of synthesized speech.
- **"What models are you using?"** — `get_config` answers.
- **"Switch to Seedream 4.0, 2048x2048."** — `set_config` saves it; the next image uses it.
| Tool | What it does |
| --- | --- |
| `generate_image` | Generate from a prompt, save it, return the paths. Optional per-call `model`, `size`, `outputDir`. |
| `generate_video` | Generate a short clip. Text to video only. Optional per-call `model`, `ratio`, `duration`, `outputDir`. Slow and paid — ask for it explicitly. |
| `generate_speech` | Synthesize speech from text, save it, return the path. Optional per-call `model`, `voice`, `format`, `outputDir`. Paid — ask for it explicitly. |
| `get_config` | Show the settings in effect, whether a key is configured, and which models are known. |
| `set_config` | Change a setting and save it. Applies to the next call. |
It knows a size each image model will refuse, and says so **before** spending your money:
```
model doubao-seedream-4-5-251128 requires at least 3686400 pixels, but 1024x1024 is only
1048576. Retry with a larger size such as 1920x1920.
```
## Requirements
Node.js `^22.19.0 || >=24.0.0`. No dependencies, no build step: clone it and run it.
## Setup
### 1. Get your API keys
The built-in preset is Volcengine Ark (Doubao / Seedream / Seedance). The server speaks the OpenAI
images API for pictures and Ark's task API for video, so a different vendor works too — it needs one
entry in `src/providers.mjs`.
Speech is a **separate service with its own key**. The Ark key is rejected there (you get
`Invalid X-Api-Key`), so if you want TTS, create a key in the Doubao speech console:
<https://console.volcengine.com/speech/new/setting/apikeys>. Image and video keep working with the
Ark key alone; you only need the second key when you call `generate_speech`.
### 2. Write the config file
This is where your keys live. `~/.config/media-gen/config.yml` on macOS and Linux, or
`%APPDATA%\media-gen\config.yml` on Windows:
```yaml
provider: ark
apiKey: your-ark-api-key # images and video
imageModel: doubao-seedream-5-0-flash-260915
imageSize: 1024x1024
videoModel: doubao-seedance-2-5-260628
# speechApiKey: your-speech-key # optional; omit to reuse apiKey
speechModel: seed-tts-2.0
speechVoice: zh_female_vv_uranus_bigtts
speechFormat: mp3
# outputDir: "~/Pictures/generated"
```
Then `chmod 600` it. [`config.example.yml`](config.example.yml) is the same thing to copy from, and
`MEDIA_GEN_CONFIG` points somewhere else if you prefer.
Quote a value that starts with `~`, otherwise YAML reads it as nothing.
### 3. Point your client at the server
Any MCP client:
```jsonc
{
"mcpServers": {
"media": {
"command": "node",
"args": ["/absolute/path/to/dsh-media-mcp/src/server.mjs"],
"cwd": "/absolute/path/to/your/workspace"
}
}
}
```
DeepSeek Harness — append this row to `$DSH_HOME/profiles/<profile>/cordis.patch.yml`.
`@deepseek-ai/dsh-mcp-client` already ships with `dsh`, so nothing needs installing:
```yaml
- insert:
- id: mcp-media
name: '@deepseek-ai/dsh-mcp-client'
config:
transport: stdio
serverName: media
command: node
args:
- /absolute/path/to/dsh-media-mcp/src/server.mjs
cwd: /absolute/path/to/your/workspace
toolCallTimeoutMs: 900000 # a video waits minutes; the 60s default is far too short
failOnStartupError: true
```
The tools appear as `mcp__media__generate_image`, `mcp__media__generate_video`,
`mcp__media__generate_speech`, `mcp__media__get_config` and `mcp__media__set_config`.
**No app restart needed:** a patched profile picks the row up in the same session, and generated
files follow `cwd`, so point `cwd` at the workspace you want them in. (Editing the server's own
`src/*.mjs` is different: that code loads once per process, so restart the server or the app.)
## Where your files go
| What | Default | Override |
| --- | --- | --- |
| Images | `<workspace>/image_output/` — one `img-YYYYMMDD-HHMMSS.jpg` per result | `outputDir` (config or per call) |
| Videos | `<workspace>/video_output/` — one `vid-YYYYMMDD-HHMMSS.mp4` per result | `outputDir` (config or per call) |
| Speech | `<workspace>/speech_output/` — one `speech-YYYYMMDD-HHMMSS.mp3` per call | `outputDir` (config or per call) |
`<workspace>` is the `cwd` you gave the server. Setting `outputDir` sends **all three** kinds to that
one folder; leave it unset to keep them apart. `media-gen outdir` prints every directory, and `~` is
expanded in a configured path.
Tools return **paths**, not file data, so a large image or clip never floods the conversation. Open
the file with any viewer; in DeepSeek Harness you can also hand an image path to the `read_image`
tool.
## Settings
Change any of these with `set_config`, or by editing the file — it is read on every call, so nothing
needs restarting either way.
| Setting | In the config file | Environment variable | Notes |
| --- | --- | --- | --- |
| Provider | `provider:` | `MEDIA_GEN_PROVIDER` | Which preset to use. Default `ark`. |
| API key | `apiKey:` | `MEDIA_GEN_API_KEY` | Required for images and video. Never printed by any tool. |
| API URL | `baseUrl:` | `MEDIA_GEN_BASE_URL` | Overrides the preset's URL. |
| Image model | `imageModel:` | `MEDIA_GEN_IMAGE_MODEL` | Default model for pictures. |
| Image size | `imageSize:` | `MEDIA_GEN_IMAGE_SIZE` | Default size, `WxH`. |
| Video model | `videoModel:` | `MEDIA_GEN_VIDEO_MODEL` | Default model for clips. |
| Speech key | `speechApiKey:` | `MEDIA_GEN_SPEECH_API_KEY` | Speech service only. Leave unset to reuse `apiKey`. |
| Speech model | `speechModel:` | `MEDIA_GEN_SPEECH_MODEL` | Speech resource id, default `seed-tts-2.0`. |
| Speech voice | `speechVoice:` | `MEDIA_GEN_SPEECH_VOICE` | Speaker id, default `zh_female_vv_uranus_bigtts`. Any id your account activated works. |
| Speech format | `speechFormat:` | `MEDIA_GEN_SPEECH_FORMAT` | `mp3`, `pcm` or `ogg_opus`. Default `mp3`. |
| Image timeout | `imageTimeoutMs:` | `MEDIA_GEN_IMAGE_TIMEOUT_MS` | Milliseconds one image call may take. Default `180000`. |
| Video timeout | `videoTimeoutMs:` | `MEDIA_GEN_VIDEO_TIMEOUT_MS` | Milliseconds the whole video flow may take. Default `720000`. |
| Speech timeout | `speechTimeoutMs:` | `MEDIA_GEN_SPEECH_TIMEOUT_MS` | Milliseconds one speech call may take. Default `120000`. |
| Output folder | `outputDir:` | `MEDIA_GEN_OUTPUT_DIR` | Overrides all the defaults above. `~` is expanded. |
An environment variable beats the config file. For the model lists, their size floors and their
notes, run `media-gen models` or ask `get_config`.
Aspect ratio and clip length are per call (`ratio`, `duration`; default `16:9` and `5` seconds),
because they change what a clip costs. The same is true of `voice` and `format` for speech.
### Timeouts
A five second clip measured about four minutes, so the waits are yours to set rather than constants
baked into the code:
```yaml
imageTimeoutMs: 300000 # a slow image provider
videoTimeoutMs: 1200000 # longer clips
speechTimeoutMs: 180000 # a long text
```
`set_config` writes the same keys, `media-gen config set videoTimeoutMs=1200000` does it from a
shell, and any of them can be overridden for one call: the generate tools take `timeoutMs`, the CLI
takes `--timeout-ms 1200000`. `get_config` reports what is in effect.
The ceiling is `3600000` (one hour). A larger value is refused rather than shortened, because a call
that hangs for an hour is a mistake worth seeing.
**Keep your client's tool timeout above ours.** The server can only stop its own wait; if your MCP
client gives up first you get its error instead of a message naming the video task id. The DSH row
above uses `toolCallTimeoutMs: 900000` for that reason.
A config file from an older release, written as `MEDIA_GEN_API_KEY=...` lines, still works. Renaming
it to `config.yml` is all it takes to move on.
## Command line
The same server comes with a small CLI. Symlink `bin/media-gen.mjs` onto your `PATH`:
```bash
media-gen env # what is in effect right now
media-gen models # image models (size floors) and speech models
media-gen outdir # where images, videos and speech will be written
media-gen img "a corgi surfing at sunset"
media-gen img "cyberpunk alley" -s 2048x2048 -o alley.jpg
media-gen video "a paper boat drifting down a rainy street" --duration 5
media-gen speech "你好,这是一段语音合成测试。"
media-gen speech "Read me aloud" -v zh_female_vv_uranus_bigtts -f mp3 -o hello.mp3
media-gen config set imageModel=doubao-seedream-4-0-250828
media-gen config set videoModel=doubao-seedance-1-0-pro-250528
media-gen config set speechApiKey=your-speech-key # write-only, like the tools
media-gen key # redacted; the raw key is never printed
media-gen serve # run the MCP server on stdio
```
## Troubleshooting
| What you see | What it means |
| --- | --- |
| `no API key configured` | `apiKey:` is missing or empty in the config file. |
| `model … requires at least N pixels` | That image model has a size floor — use the size the message suggests. |
| `media API error [InvalidEndpointOrModel.NotFound]` | The model is not available to your account or region. `get_config` lists the models this version knows. |
| `media API error [SetLimitExceeded]` | Ark's "Safe Experience Mode" usage cap for that model. Raise or close it on the Ark console's model-activation page. |
| `speech API error [45000010]: Invalid X-Api-Key` | The speech service rejected the key. An Ark key does not work there: set `speechApiKey:` to a key from the Doubao speech console. |
| `speech API error [55000000]: resource ID is mismatched with speaker related resource` | The voice does not belong to the configured speech resource. Check the voice and resource id against the speech console — either can be the mismatched one. |
| `speech API returned no audio data` | The service answered without audio — usually the same credential or resource problem in a different shape. The message quotes the raw body. |
| `video task … did not finish within Ns` | The clip was still rendering. The message carries the task id, so you can query it instead of paying for a second render — or raise `videoTimeoutMs`. |
| `video task … reported a status this version does not know` | A state the server will not guess at. Update the server rather than retrying blindly. |
| A video tool call is cut off by your client | Raise its tool timeout. The clip legitimately takes minutes; the row above uses `900000`. |
| `ignored: <key>` in `get_config` | A config key that is not a setting — usually a typo, so it changed nothing. |
| `format: env assignments — legacy` | The file is in the old `KEY=value` style. It still works; rename it to `config.yml` whenever you like. |
## What video can and cannot do
Text to video, one clip per call. Reference images and videos (image-to-video, extend, edit) are not
supported: each of those changes the request contract, and what is not implemented should not look
like it is.
## What speech can and cannot do
Text to speech, one file per call, using the streaming endpoint the provider documents — the whole
answer is collected and written once, so a partial file is never left behind. Voice, format and
sampling rate are settings; speed, pitch, loudness and emotion controls, voice cloning and
speech-to-text are **not** implemented, so nothing pretends to accept them.
## Development
Tests are offline — no network, no API key, no spend:
```bash
node --test tests/*.test.mjs
```
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 4 tools
Each tool targets a distinct operation: image generation, video generation, config retrieval, and config modification. No overlap exists, making selection unambiguous.
All tools follow the verb_noun pattern: generate_image, generate_video, get_config, set_config. The naming is consistent and predictable.
With 4 tools, the server is well-scoped for its purpose—media generation plus configuration management. Each tool is essential and there are no redundant or missing core operations.
The core generation and configuration workflows are covered, but the reference to a read_image tool in generate_image's description suggests a potential gap for viewing outputs. No explicit file listing or deletion tools are present, though these may be outside the server's intended scope.