turbogen-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ .env
2
+ .venv/
3
+ data/
4
+ __pycache__/
5
+ *.pyc
6
+ gpu-worker/models/
7
+ gpu-worker/token.txt
8
+ gpu-worker/venv/
9
+ gpu-worker/logs/
10
+ tests/gpu_live/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Turbogen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.5
2
+ Name: turbogen-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for generating images, video and audio through the Turbogen API
5
+ Project-URL: Homepage, https://turbogen.app
6
+ Project-URL: Documentation, https://turbogen.app/api-docs#mcp
7
+ Project-URL: Pricing, https://turbogen.app/pricing
8
+ Author-email: Turbogen <hello@turbogen.app>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-image,ai-video,claude,kling,mcp,model-context-protocol,nano-banana,seedance,veo
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Classifier: Topic :: Multimedia :: Video
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: mcp>=1.2
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Turbogen MCP server
22
+
23
+ <!-- mcp-name: app.turbogen/turbogen -->
24
+
25
+ Generate images, video and audio with Turbogen straight from Claude Desktop, Claude Code, Cursor or any
26
+ other MCP client. Same models and prices as the studio; credits come from your balance.
27
+
28
+ ## Tools
29
+
30
+ | Tool | What it does |
31
+ |---|---|
32
+ | `list_models(kind?)` | Model ids, aspect/voice choices, options and credit prices (`kind` = image, video or audio) |
33
+ | `generate_image(prompt, model="nano-banana-2", aspect="1:1", option?, reference_images?)` | Image, saved locally; a preview is returned when the file is under 1 MB |
34
+ | `generate_video(prompt, model="veo-3.1-fast", aspect="16:9", option="8s", image_path?)` | Video clip; pass `image_path` for image-to-video |
35
+ | `generate_audio(text_or_prompt, model="voice", aspect?, option?, lyrics?)` | `voice` (aspect = voice name), `music` (aspect = Instrumental / With vocals + lyrics) or `sfx` |
36
+ | `upscale_image(path, scale="2x")` | Upscale a local image 2x or 4x |
37
+ | `get_balance()` | Remaining credits |
38
+ | `job_status(job_id, download=false)` | Status of a job, e.g. one that took longer than the 10-minute wait |
39
+
40
+ Generation tools submit the job, poll every 3 s for up to 10 minutes, download the result into the output
41
+ folder and return the local path, the cost and your remaining credits.
42
+
43
+ ## Get an API key
44
+
45
+ API access comes with the Boost, Turbo and Max packs. Open **API** in the studio
46
+ (`https://turbogen.app/#/apikeys`), create a key and copy it (`lk_...`, shown only once).
47
+
48
+ ## Configuration
49
+
50
+ | Variable | Required | Default |
51
+ |---|---|---|
52
+ | `TURBOGEN_API_KEY` | yes | — |
53
+ | `TURBOGEN_BASE_URL` | no | `https://turbogen.app` |
54
+ | `TURBOGEN_OUTPUT_DIR` | no | `~/Downloads/turbogen` |
55
+
56
+ ## Install
57
+
58
+ With [uv](https://docs.astral.sh/uv/) nothing needs installing up front, `uvx` runs it on demand:
59
+
60
+ ```bash
61
+ uvx turbogen-mcp
62
+ ```
63
+
64
+ Or install the command once with pipx (or uv tool):
65
+
66
+ ```bash
67
+ pipx install turbogen-mcp # or: uv tool install turbogen-mcp
68
+ turbogen-mcp # waits for an MCP client on stdin; Ctrl+C to quit
69
+ ```
70
+
71
+ Plain venv:
72
+
73
+ ```bash
74
+ python -m venv .venv
75
+ .venv/bin/pip install turbogen-mcp # Windows: .venv\Scripts\pip install turbogen-mcp
76
+ ```
77
+
78
+ Requires Python 3.10+.
79
+
80
+ ## Claude Desktop
81
+
82
+ Edit `claude_desktop_config.json` (Settings > Developer > Edit Config; macOS:
83
+ `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`) and restart Claude Desktop:
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "turbogen": {
89
+ "command": "uvx",
90
+ "args": ["turbogen-mcp"],
91
+ "env": {
92
+ "TURBOGEN_API_KEY": "lk_your_key_here"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ With pipx use `"command": "turbogen-mcp"` and drop `args`. If the app cannot find the command, give the full
100
+ path (e.g. `C:\\Users\\you\\.local\\bin\\turbogen-mcp.exe`).
101
+
102
+ ## Claude Code
103
+
104
+ ```bash
105
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- uvx turbogen-mcp
106
+ # installed with pipx instead:
107
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- turbogen-mcp
108
+ ```
109
+
110
+ Add `--scope user` to make it available in every project. Check with `claude mcp list`.
111
+
112
+ ## Cursor
113
+
114
+ Create `.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for all projects):
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "turbogen": {
120
+ "command": "uvx",
121
+ "args": ["turbogen-mcp"],
122
+ "env": {
123
+ "TURBOGEN_API_KEY": "lk_your_key_here",
124
+ "TURBOGEN_OUTPUT_DIR": "~/Downloads/turbogen"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Try it
132
+
133
+ Ask your assistant things like:
134
+
135
+ - "List the Turbogen video models and their prices."
136
+ - "Generate a 16:9 image of a lighthouse at dusk with nano-banana-2."
137
+ - "Turn ~/Downloads/turbogen/lighthouse.png into an 8 second Veo clip with a slow push-in."
138
+ - "Make a 5 second sound effect of waves on rocks."
139
+ - "How many credits do I have left?"
140
+
141
+ ## Errors
142
+
143
+ - *Not enough credits* (402): top up at `<base>/#/pricing`.
144
+ - *Invalid API key* (401): the key is wrong, revoked, or the account has no API access.
145
+ - *Model unavailable* (503): try another model or retry later.
146
+ - *Rate limited* (429): up to 30 jobs per minute and 3 running at once.
147
+ - Failed generations are refunded automatically.
148
+
149
+ ## Links
150
+
151
+ - Website and pricing: https://turbogen.app/pricing
152
+ - API reference: https://turbogen.app/api-docs
153
+ - Support: hello@turbogen.app
@@ -0,0 +1,133 @@
1
+ # Turbogen MCP server
2
+
3
+ <!-- mcp-name: app.turbogen/turbogen -->
4
+
5
+ Generate images, video and audio with Turbogen straight from Claude Desktop, Claude Code, Cursor or any
6
+ other MCP client. Same models and prices as the studio; credits come from your balance.
7
+
8
+ ## Tools
9
+
10
+ | Tool | What it does |
11
+ |---|---|
12
+ | `list_models(kind?)` | Model ids, aspect/voice choices, options and credit prices (`kind` = image, video or audio) |
13
+ | `generate_image(prompt, model="nano-banana-2", aspect="1:1", option?, reference_images?)` | Image, saved locally; a preview is returned when the file is under 1 MB |
14
+ | `generate_video(prompt, model="veo-3.1-fast", aspect="16:9", option="8s", image_path?)` | Video clip; pass `image_path` for image-to-video |
15
+ | `generate_audio(text_or_prompt, model="voice", aspect?, option?, lyrics?)` | `voice` (aspect = voice name), `music` (aspect = Instrumental / With vocals + lyrics) or `sfx` |
16
+ | `upscale_image(path, scale="2x")` | Upscale a local image 2x or 4x |
17
+ | `get_balance()` | Remaining credits |
18
+ | `job_status(job_id, download=false)` | Status of a job, e.g. one that took longer than the 10-minute wait |
19
+
20
+ Generation tools submit the job, poll every 3 s for up to 10 minutes, download the result into the output
21
+ folder and return the local path, the cost and your remaining credits.
22
+
23
+ ## Get an API key
24
+
25
+ API access comes with the Boost, Turbo and Max packs. Open **API** in the studio
26
+ (`https://turbogen.app/#/apikeys`), create a key and copy it (`lk_...`, shown only once).
27
+
28
+ ## Configuration
29
+
30
+ | Variable | Required | Default |
31
+ |---|---|---|
32
+ | `TURBOGEN_API_KEY` | yes | — |
33
+ | `TURBOGEN_BASE_URL` | no | `https://turbogen.app` |
34
+ | `TURBOGEN_OUTPUT_DIR` | no | `~/Downloads/turbogen` |
35
+
36
+ ## Install
37
+
38
+ With [uv](https://docs.astral.sh/uv/) nothing needs installing up front, `uvx` runs it on demand:
39
+
40
+ ```bash
41
+ uvx turbogen-mcp
42
+ ```
43
+
44
+ Or install the command once with pipx (or uv tool):
45
+
46
+ ```bash
47
+ pipx install turbogen-mcp # or: uv tool install turbogen-mcp
48
+ turbogen-mcp # waits for an MCP client on stdin; Ctrl+C to quit
49
+ ```
50
+
51
+ Plain venv:
52
+
53
+ ```bash
54
+ python -m venv .venv
55
+ .venv/bin/pip install turbogen-mcp # Windows: .venv\Scripts\pip install turbogen-mcp
56
+ ```
57
+
58
+ Requires Python 3.10+.
59
+
60
+ ## Claude Desktop
61
+
62
+ Edit `claude_desktop_config.json` (Settings > Developer > Edit Config; macOS:
63
+ `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`) and restart Claude Desktop:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "turbogen": {
69
+ "command": "uvx",
70
+ "args": ["turbogen-mcp"],
71
+ "env": {
72
+ "TURBOGEN_API_KEY": "lk_your_key_here"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ With pipx use `"command": "turbogen-mcp"` and drop `args`. If the app cannot find the command, give the full
80
+ path (e.g. `C:\\Users\\you\\.local\\bin\\turbogen-mcp.exe`).
81
+
82
+ ## Claude Code
83
+
84
+ ```bash
85
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- uvx turbogen-mcp
86
+ # installed with pipx instead:
87
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- turbogen-mcp
88
+ ```
89
+
90
+ Add `--scope user` to make it available in every project. Check with `claude mcp list`.
91
+
92
+ ## Cursor
93
+
94
+ Create `.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for all projects):
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "turbogen": {
100
+ "command": "uvx",
101
+ "args": ["turbogen-mcp"],
102
+ "env": {
103
+ "TURBOGEN_API_KEY": "lk_your_key_here",
104
+ "TURBOGEN_OUTPUT_DIR": "~/Downloads/turbogen"
105
+ }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## Try it
112
+
113
+ Ask your assistant things like:
114
+
115
+ - "List the Turbogen video models and their prices."
116
+ - "Generate a 16:9 image of a lighthouse at dusk with nano-banana-2."
117
+ - "Turn ~/Downloads/turbogen/lighthouse.png into an 8 second Veo clip with a slow push-in."
118
+ - "Make a 5 second sound effect of waves on rocks."
119
+ - "How many credits do I have left?"
120
+
121
+ ## Errors
122
+
123
+ - *Not enough credits* (402): top up at `<base>/#/pricing`.
124
+ - *Invalid API key* (401): the key is wrong, revoked, or the account has no API access.
125
+ - *Model unavailable* (503): try another model or retry later.
126
+ - *Rate limited* (429): up to 30 jobs per minute and 3 running at once.
127
+ - Failed generations are refunded automatically.
128
+
129
+ ## Links
130
+
131
+ - Website and pricing: https://turbogen.app/pricing
132
+ - API reference: https://turbogen.app/api-docs
133
+ - Support: hello@turbogen.app
@@ -0,0 +1,129 @@
1
+ # Turbogen MCP server
2
+
3
+ Generate images, video and audio with Turbogen straight from Claude Desktop, Claude Code, Cursor or any
4
+ other MCP client. Same models and prices as the studio; credits come from your balance.
5
+
6
+ ## Tools
7
+
8
+ | Tool | What it does |
9
+ |---|---|
10
+ | `list_models(kind?)` | Model ids, aspect/voice choices, options and credit prices (`kind` = image, video or audio) |
11
+ | `generate_image(prompt, model="nano-banana-2", aspect="1:1", option?, reference_images?)` | Image, saved locally; a preview is returned when the file is under 1 MB |
12
+ | `generate_video(prompt, model="veo-3.1-fast", aspect="16:9", option="8s", image_path?)` | Video clip; pass `image_path` for image-to-video |
13
+ | `generate_audio(text_or_prompt, model="voice", aspect?, option?, lyrics?)` | `voice` (aspect = voice name), `music` (aspect = Instrumental / With vocals + lyrics) or `sfx` |
14
+ | `upscale_image(path, scale="2x")` | Upscale a local image 2x or 4x |
15
+ | `get_balance()` | Remaining credits |
16
+ | `job_status(job_id, download=false)` | Status of a job, e.g. one that took longer than the 10-minute wait |
17
+
18
+ Generation tools submit the job, poll every 3 s for up to 10 minutes, download the result into the output
19
+ folder and return the local path, the cost and your remaining credits.
20
+
21
+ ## Get an API key
22
+
23
+ API access comes with the Boost, Turbo and Max packs. Open **API** in the studio
24
+ (`https://turbogen.app/#/apikeys`), create a key and copy it (`lk_...`, shown only once).
25
+
26
+ ## Configuration
27
+
28
+ | Variable | Required | Default |
29
+ |---|---|---|
30
+ | `TURBOGEN_API_KEY` | yes | — |
31
+ | `TURBOGEN_BASE_URL` | no | `https://turbogen.app` |
32
+ | `TURBOGEN_OUTPUT_DIR` | no | `~/Downloads/turbogen` |
33
+
34
+ ## Install
35
+
36
+ With [uv](https://docs.astral.sh/uv/) nothing needs installing up front, `uvx` runs it on demand:
37
+
38
+ ```bash
39
+ uvx --from /path/to/mcp-server turbogen-mcp
40
+ ```
41
+
42
+ Or install the command once with pipx (or uv tool):
43
+
44
+ ```bash
45
+ pipx install /path/to/mcp-server # or: uv tool install /path/to/mcp-server
46
+ turbogen-mcp # waits for an MCP client on stdin; Ctrl+C to quit
47
+ ```
48
+
49
+ Plain venv:
50
+
51
+ ```bash
52
+ python -m venv .venv
53
+ .venv/bin/pip install . # Windows: .venv\Scripts\pip install .
54
+ ```
55
+
56
+ Requires Python 3.10+.
57
+
58
+ ## Claude Desktop
59
+
60
+ Edit `claude_desktop_config.json` (Settings > Developer > Edit Config; macOS:
61
+ `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`) and restart Claude Desktop:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "turbogen": {
67
+ "command": "uvx",
68
+ "args": ["--from", "/path/to/mcp-server", "turbogen-mcp"],
69
+ "env": {
70
+ "TURBOGEN_API_KEY": "lk_your_key_here"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ With pipx use `"command": "turbogen-mcp"` and drop `args`. If the app cannot find the command, give the full
78
+ path (e.g. `C:\\Users\\you\\.local\\bin\\turbogen-mcp.exe`).
79
+
80
+ ## Claude Code
81
+
82
+ ```bash
83
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- uvx --from /path/to/mcp-server turbogen-mcp
84
+ # installed with pipx instead:
85
+ claude mcp add turbogen --env TURBOGEN_API_KEY=lk_your_key_here -- turbogen-mcp
86
+ ```
87
+
88
+ Add `--scope user` to make it available in every project. Check with `claude mcp list`.
89
+
90
+ ## Cursor
91
+
92
+ Create `.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for all projects):
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "turbogen": {
98
+ "command": "uvx",
99
+ "args": ["--from", "/path/to/mcp-server", "turbogen-mcp"],
100
+ "env": {
101
+ "TURBOGEN_API_KEY": "lk_your_key_here",
102
+ "TURBOGEN_OUTPUT_DIR": "~/Downloads/turbogen"
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Try it
110
+
111
+ Ask your assistant things like:
112
+
113
+ - "List the Turbogen video models and their prices."
114
+ - "Generate a 16:9 image of a lighthouse at dusk with nano-banana-2."
115
+ - "Turn ~/Downloads/turbogen/lighthouse.png into an 8 second Veo clip with a slow push-in."
116
+ - "Make a 5 second sound effect of waves on rocks."
117
+ - "How many credits do I have left?"
118
+
119
+ ## Errors
120
+
121
+ - *Not enough credits* (402): top up at `<base>/#/pricing`.
122
+ - *Invalid API key* (401): the key is wrong, revoked, or the account has no API access.
123
+ - *Model unavailable* (503): try another model or retry later.
124
+ - *Rate limited* (429): up to 30 jobs per minute and 3 running at once.
125
+ - Failed generations are refunded automatically.
126
+
127
+ ## Rebranding
128
+
129
+ The brand name and default host are the `BRAND` and `DEFAULT_BASE_URL` constants at the top of `server.py`.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "turbogen-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for generating images, video and audio through the Turbogen API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Turbogen", email = "hello@turbogen.app" }]
14
+ keywords = ["mcp", "model-context-protocol", "ai-video", "ai-image", "veo", "seedance", "kling", "nano-banana", "claude"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Multimedia :: Video",
19
+ "Topic :: Multimedia :: Graphics",
20
+ ]
21
+ dependencies = [
22
+ "mcp>=1.2",
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://turbogen.app"
28
+ Documentation = "https://turbogen.app/api-docs#mcp"
29
+ Pricing = "https://turbogen.app/pricing"
30
+
31
+ [project.scripts]
32
+ turbogen-mcp = "server:main"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ only-include = ["server.py", "LICENSE"]
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "app.turbogen/turbogen",
4
+ "title": "Turbogen",
5
+ "description": "Generate images, video and audio (Veo 3.1, Seedance, Kling, Nano Banana Pro) pay-as-you-go.",
6
+ "websiteUrl": "https://turbogen.app/api-docs#mcp",
7
+ "version": "0.1.0",
8
+ "packages": [
9
+ {
10
+ "registryType": "pypi",
11
+ "identifier": "turbogen-mcp",
12
+ "version": "0.1.0",
13
+ "transport": { "type": "stdio" },
14
+ "environmentVariables": [
15
+ { "name": "TURBOGEN_API_KEY", "description": "API key from https://turbogen.app/#/apikeys (starts with lk_)", "isRequired": true, "isSecret": true },
16
+ { "name": "TURBOGEN_OUTPUT_DIR", "description": "Folder for downloaded results (default ~/Downloads/turbogen)", "isRequired": false }
17
+ ]
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,378 @@
1
+ """Turbogen MCP server: generate images, video and audio through the Turbogen API from any MCP client.
2
+
3
+ Config (environment):
4
+ TURBOGEN_API_KEY required, an API key starting with lk_
5
+ TURBOGEN_BASE_URL optional, defaults to DEFAULT_BASE_URL
6
+ TURBOGEN_OUTPUT_DIR optional, defaults to ~/Downloads/turbogen
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import mimetypes
12
+ import os
13
+ import re
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import httpx
19
+ try: # mcp 1.x
20
+ from mcp.server.fastmcp import FastMCP, Image
21
+ except ImportError: # mcp 2.x renamed FastMCP -> MCPServer (same decorator API)
22
+ from mcp.server.mcpserver import Image, MCPServer as FastMCP
23
+
24
+ # Brand / host live here only - change these two lines on a rename.
25
+ BRAND = "Turbogen"
26
+ DEFAULT_BASE_URL = "https://turbogen.app"
27
+
28
+ POLL_EVERY_S = 3
29
+ POLL_TIMEOUT_S = 600
30
+ INLINE_IMAGE_MAX_BYTES = 1_000_000
31
+
32
+ mcp = FastMCP(f"{BRAND.lower()}")
33
+
34
+
35
+ # ---------- config & http ----------
36
+
37
+ class TurbogenError(Exception):
38
+ pass
39
+
40
+
41
+ def _base() -> str:
42
+ return os.environ.get("TURBOGEN_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
43
+
44
+
45
+ def _key() -> str:
46
+ k = os.environ.get("TURBOGEN_API_KEY", "").strip()
47
+ if not k:
48
+ raise TurbogenError("TURBOGEN_API_KEY is not set. Create a key on the API page of "
49
+ f"{_base()}/#/apikeys and add it to the MCP server config.")
50
+ return k
51
+
52
+
53
+ def _out_dir() -> Path:
54
+ d = Path(os.environ.get("TURBOGEN_OUTPUT_DIR") or Path.home() / "Downloads" / BRAND.lower()).expanduser()
55
+ d.mkdir(parents=True, exist_ok=True)
56
+ return d
57
+
58
+
59
+ def _client(auth: bool = True) -> httpx.AsyncClient:
60
+ headers = {"User-Agent": f"{BRAND.lower()}-mcp/0.1"}
61
+ if auth:
62
+ headers["Authorization"] = f"Bearer {_key()}"
63
+ return httpx.AsyncClient(base_url=_base(), headers=headers, timeout=httpx.Timeout(120, connect=15),
64
+ follow_redirects=True)
65
+
66
+
67
+ def _raise_for(r: httpx.Response) -> None:
68
+ if r.is_success:
69
+ return
70
+ try:
71
+ detail = r.json().get("detail")
72
+ except Exception:
73
+ detail = None
74
+ detail = detail if isinstance(detail, str) else (r.text[:300] or r.reason_phrase)
75
+ if r.status_code == 402:
76
+ raise TurbogenError(f"Not enough credits, top up at {_base()}/#/pricing")
77
+ if r.status_code == 401:
78
+ raise TurbogenError(f"Invalid API key ({detail}). Check TURBOGEN_API_KEY; keys are managed at {_base()}/#/apikeys")
79
+ if r.status_code == 503:
80
+ raise TurbogenError(f"Model unavailable: {detail}")
81
+ if r.status_code == 429:
82
+ raise TurbogenError(f"Rate limited: {detail}")
83
+ raise TurbogenError(f"API error {r.status_code}: {detail}")
84
+
85
+
86
+ _models_cache: dict[str, Any] = {"t": 0.0, "data": None}
87
+
88
+
89
+ async def _catalog() -> dict:
90
+ if _models_cache["data"] and time.time() - _models_cache["t"] < 300:
91
+ return _models_cache["data"]
92
+ # /api/models is public; API keys are accepted on /api/jobs only.
93
+ async with _client(auth=False) as c:
94
+ r = await c.get("/api/models")
95
+ _raise_for(r)
96
+ _models_cache.update(t=time.time(), data=r.json())
97
+ return _models_cache["data"]
98
+
99
+
100
+ async def _model(model_id: str, kind: str | None = None) -> dict:
101
+ cat = await _catalog()
102
+ for m in cat["models"]:
103
+ if m["id"] == model_id:
104
+ if kind and m["kind"] != kind:
105
+ raise TurbogenError(f"'{model_id}' is a {m['kind']} model, not {kind}. Use list_models(kind='{kind}').")
106
+ return m
107
+ ids = ", ".join(m["id"] for m in cat["models"] if not kind or m["kind"] == kind)
108
+ raise TurbogenError(f"Unknown model '{model_id}'. Available: {ids}")
109
+
110
+
111
+ def _pick(value: str | None, allowed: list[str], label: str, model_id: str) -> str:
112
+ if not value:
113
+ return allowed[0]
114
+ if value in allowed:
115
+ return value
116
+ for a in allowed: # forgiving case-insensitive match ("4k" -> "4K")
117
+ if a.lower() == value.lower():
118
+ return a
119
+ raise TurbogenError(f"{label} '{value}' is not supported by {model_id}. Choose one of: {', '.join(allowed)}")
120
+
121
+
122
+ def _ref_file(path: str) -> tuple[str, bytes, str]:
123
+ p = Path(path).expanduser()
124
+ if not p.is_file():
125
+ raise TurbogenError(f"File not found: {p}")
126
+ mime = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
127
+ if mime not in ("image/png", "image/jpeg", "image/webp"):
128
+ raise TurbogenError(f"{p.name}: only PNG, JPEG or WEBP images are accepted")
129
+ return (p.name, p.read_bytes(), mime)
130
+
131
+
132
+ # ---------- job flow ----------
133
+
134
+ async def _find_job(c: httpx.AsyncClient, job_id: str) -> tuple[dict | None, int]:
135
+ r = await c.get("/api/jobs", params={"limit": 200})
136
+ _raise_for(r)
137
+ data = r.json()
138
+ return next((j for j in data["jobs"] if j["id"] == job_id), None), data.get("credits")
139
+
140
+
141
+ def _slug(s: str, n: int = 40) -> str:
142
+ s = re.sub(r"[^A-Za-z0-9]+", "-", s).strip("-").lower()
143
+ return s[:n].strip("-") or "result"
144
+
145
+
146
+ async def _run_job(fields: dict[str, str], refs: list[str], label: str) -> dict:
147
+ files = [("refs", _ref_file(p)) for p in refs]
148
+ async with _client() as c:
149
+ r = await c.post("/api/jobs", data=fields, files=files or None)
150
+ _raise_for(r)
151
+ sub = r.json()
152
+ job = sub["job"]
153
+ credits = sub.get("credits")
154
+ deadline = time.monotonic() + POLL_TIMEOUT_S
155
+ while job["status"] in ("queued", "running"):
156
+ if time.monotonic() > deadline:
157
+ raise TurbogenError(f"Job {job['id']} still {job['status']} after {POLL_TIMEOUT_S // 60} min. "
158
+ f"Check later with job_status('{job['id']}').")
159
+ await asyncio.sleep(POLL_EVERY_S)
160
+ found, credits = await _find_job(c, job["id"])
161
+ if found:
162
+ job = found
163
+ if job["status"] != "done":
164
+ raise TurbogenError(f"Generation failed: {job.get('note') or 'no reason given'} "
165
+ f"(job {job['id']}; failed jobs are refunded automatically). Credits: {credits}")
166
+ path = await _download(c, job, label)
167
+ return {"job_id": job["id"], "path": str(path), "mime": job.get("mime"), "cost": job.get("cost"),
168
+ "credits_left": credits}
169
+
170
+
171
+ async def _download(c: httpx.AsyncClient, job: dict, label: str) -> Path:
172
+ r = await c.get(job["url"] or f"/api/jobs/{job['id']}/file")
173
+ _raise_for(r)
174
+ mime = (job.get("mime") or r.headers.get("content-type", "")).split(";")[0]
175
+ ext = {"image/jpeg": ".jpg", "audio/mpeg": ".mp3"}.get(mime) or mimetypes.guess_extension(mime) or ".bin"
176
+ path = _out_dir() / f"{job['model']}-{_slug(label)}-{job['id'][:8]}{ext}"
177
+ path.write_bytes(r.content)
178
+ return path
179
+
180
+
181
+ def _summary(res: dict) -> str:
182
+ return (f"Saved: {res['path']}\nCost: {res['cost']} credits · Credits left: {res['credits_left']}"
183
+ f"\nJob: {res['job_id']}")
184
+
185
+
186
+ def _with_image(res: dict) -> list:
187
+ out: list = [_summary(res)]
188
+ p = Path(res["path"])
189
+ if (res.get("mime") or "").startswith("image/") and p.stat().st_size <= INLINE_IMAGE_MAX_BYTES:
190
+ out.append(Image(path=p))
191
+ return out
192
+
193
+
194
+ def _err(e: Exception) -> str:
195
+ if isinstance(e, TurbogenError):
196
+ return f"Error: {e}"
197
+ if isinstance(e, httpx.HTTPError):
198
+ return f"Error: could not reach {_base()} ({e.__class__.__name__}: {e})"
199
+ raise e
200
+
201
+
202
+ # ---------- tools ----------
203
+
204
+ @mcp.tool()
205
+ async def list_models(kind: str | None = None) -> str:
206
+ """List available generation models with their ids, aspect/voice choices, options and credit prices.
207
+
208
+ kind: optional filter - "image", "video" or "audio".
209
+ """
210
+ try:
211
+ cat = await _catalog()
212
+ rate = cat.get("usd_per_credit")
213
+ lines = [f"{BRAND} models (1 credit ≈ ${rate})" if rate else f"{BRAND} models"]
214
+ for m in cat["models"]:
215
+ if kind and m["kind"] != kind.lower():
216
+ continue
217
+ prices = ", ".join(f"{o}={c}cr" for o, c in m["prices"].items())
218
+ bits = [f"- {m['id']} [{m['kind']}] {m['name']}",
219
+ f" {m.get('aspect_label', 'Aspect')}: {', '.join(m['aspects'])}",
220
+ f" {m.get('option_label', 'Option')}: {prices}"]
221
+ extra = []
222
+ if m.get("max_refs"):
223
+ extra.append(f"refs≤{m['max_refs']}")
224
+ if m.get("needs_ref"):
225
+ extra.append("needs an input image")
226
+ if m.get("lyrics"):
227
+ extra.append(f"lyrics for: {', '.join(m['lyrics'])}")
228
+ if not m.get("available", True):
229
+ extra.append("UNAVAILABLE now")
230
+ if extra:
231
+ bits.append(" " + " · ".join(extra))
232
+ if m.get("tagline"):
233
+ bits.append(f" {m['tagline']}")
234
+ lines.append("\n".join(bits))
235
+ return "\n".join(lines)
236
+ except Exception as e:
237
+ return _err(e)
238
+
239
+
240
+ @mcp.tool()
241
+ async def generate_image(prompt: str, model: str = "nano-banana-2", aspect: str = "1:1",
242
+ option: str | None = None, reference_images: list[str] | None = None) -> list:
243
+ """Generate an image and save it locally. Returns the file path, cost and remaining credits (plus a preview).
244
+
245
+ prompt: what to draw.
246
+ model: image model id (see list_models(kind="image")), default nano-banana-2.
247
+ aspect: aspect ratio, e.g. "1:1", "16:9", "9:16".
248
+ option: resolution/quality option such as "1K", "2K", "4K"; defaults to the model's first (cheapest) option.
249
+ reference_images: optional list of local PNG/JPEG/WEBP paths used as references.
250
+ """
251
+ try:
252
+ m = await _model(model, "image")
253
+ if m.get("needs_ref"):
254
+ return [f"Error: {model} is a tool, not a generator. Use upscale_image for it."]
255
+ refs = reference_images or []
256
+ if len(refs) > m.get("max_refs", 0):
257
+ return [f"Error: {m['name']} accepts up to {m.get('max_refs', 0)} reference images"]
258
+ fields = {"model": model, "prompt": prompt, "aspect": _pick(aspect, m["aspects"], "Aspect", model),
259
+ "option": _pick(option, list(m["prices"]), m.get("option_label", "Option"), model)}
260
+ return _with_image(await _run_job(fields, refs, prompt))
261
+ except Exception as e:
262
+ return [_err(e)]
263
+
264
+
265
+ @mcp.tool()
266
+ async def generate_video(prompt: str, model: str = "veo-3.1-fast", aspect: str = "16:9",
267
+ option: str = "8s", image_path: str | None = None) -> str:
268
+ """Generate a video clip and save it locally. Returns the file path, cost and remaining credits.
269
+
270
+ prompt: the shot - subject, action, camera move, light.
271
+ model: video model id (see list_models(kind="video")), default veo-3.1-fast.
272
+ aspect: "16:9", "9:16" (some models also "1:1" etc.).
273
+ option: duration such as "4s", "6s", "8s"; must be one the model offers.
274
+ image_path: optional local image for image-to-video (first frame / reference).
275
+ Video takes 1-5 minutes; the call waits up to 10 minutes.
276
+ """
277
+ try:
278
+ m = await _model(model, "video")
279
+ refs = [image_path] if image_path else []
280
+ if refs and not m.get("max_refs"):
281
+ return f"Error: {m['name']} does not accept an input image"
282
+ opts = list(m["prices"])
283
+ if option == "8s" and option not in opts: # default doesn't fit this model -> its first duration
284
+ option = None
285
+ fields = {"model": model, "prompt": prompt, "aspect": _pick(aspect, m["aspects"], "Aspect", model),
286
+ "option": _pick(option, opts, "Duration", model)}
287
+ return _summary(await _run_job(fields, refs, prompt))
288
+ except Exception as e:
289
+ return _err(e)
290
+
291
+
292
+ @mcp.tool()
293
+ async def generate_audio(text_or_prompt: str, model: str = "voice", aspect: str | None = None,
294
+ option: str | None = None, lyrics: str | None = None) -> str:
295
+ """Generate audio (voiceover, background music, a song or a sound effect) and save it locally.
296
+
297
+ text_or_prompt: for "voice" the text to speak; for "music"/"song"/"sfx" a description of the sound.
298
+ model: "voice" (text to speech), "music" (instrumental background), "song" (sung, needs lyrics) or "sfx".
299
+ aspect: the voice for "voice" (e.g. "Deep narrator", "Warm female"), the mood for "music" (e.g. "Cinematic",
300
+ "Lo-fi"), the vocal for "song" ("Female vocal", "Male vocal", "Duet"); defaults to the first choice.
301
+ See list_models(kind="audio").
302
+ option: length/duration, e.g. "300 chars" for voice, "30s" for music, "60s" for song, "5s" for sfx; defaults to the first.
303
+ lyrics: song lyrics, required for "song".
304
+ """
305
+ try:
306
+ m = await _model(model, "audio")
307
+ fields = {"model": model, "prompt": text_or_prompt,
308
+ "aspect": _pick(aspect, m["aspects"], m.get("aspect_label", "Aspect"), model),
309
+ "option": _pick(option, list(m["prices"]), m.get("option_label", "Option"), model)}
310
+ if fields["aspect"] in (m.get("lyrics") or []):
311
+ if not lyrics:
312
+ return f"Error: '{fields['aspect']}' needs lyrics. Pass lyrics or use aspect='Instrumental'."
313
+ fields["lyrics"] = lyrics
314
+ return _summary(await _run_job(fields, [], text_or_prompt))
315
+ except Exception as e:
316
+ return _err(e)
317
+
318
+
319
+ @mcp.tool()
320
+ async def upscale_image(path: str, scale: str = "2x") -> list:
321
+ """Upscale a local image 2x or 4x and save the result next to other outputs.
322
+
323
+ path: local PNG/JPEG/WEBP file.
324
+ scale: "2x" or "4x".
325
+ """
326
+ try:
327
+ m = await _model("upscale", "image")
328
+ fields = {"model": "upscale", "prompt": "", "aspect": m["aspects"][0],
329
+ "option": _pick(scale, list(m["prices"]), "Scale", "upscale")}
330
+ return _with_image(await _run_job(fields, [path], Path(path).stem + "-" + fields["option"]))
331
+ except Exception as e:
332
+ return [_err(e)]
333
+
334
+
335
+ @mcp.tool()
336
+ async def get_balance() -> str:
337
+ """Show the remaining credit balance of the account behind TURBOGEN_API_KEY."""
338
+ try:
339
+ async with _client() as c:
340
+ r = await c.get("/api/jobs", params={"limit": 1})
341
+ _raise_for(r)
342
+ credits = r.json().get("credits")
343
+ rate = (await _catalog()).get("usd_per_credit")
344
+ usd = f" (≈ ${credits * rate:.2f})" if rate and credits is not None else ""
345
+ return f"Credits: {credits}{usd}. Top up at {_base()}/#/pricing"
346
+ except Exception as e:
347
+ return _err(e)
348
+
349
+
350
+ @mcp.tool()
351
+ async def job_status(job_id: str, download: bool = False) -> str:
352
+ """Check a generation job by id (e.g. one that outlived the 10-minute wait).
353
+
354
+ download: if true and the job is done, save its file to the output folder and return the path.
355
+ """
356
+ try:
357
+ async with _client() as c:
358
+ job, credits = await _find_job(c, job_id)
359
+ if not job:
360
+ return f"Error: job {job_id} not found among your 200 most recent jobs"
361
+ lines = [f"Job {job['id']}: {job['status']}", f"Model: {job['model']} · Cost: {job.get('cost')} credits",
362
+ f"Created: {job.get('created_at')}"]
363
+ if job.get("note"):
364
+ lines.append(f"Note: {job['note']}")
365
+ if job["status"] == "done" and download:
366
+ lines.append(f"Saved: {await _download(c, job, job.get('prompt') or 'result')}")
367
+ lines.append(f"Credits left: {credits}")
368
+ return "\n".join(lines)
369
+ except Exception as e:
370
+ return _err(e)
371
+
372
+
373
+ def main() -> None:
374
+ mcp.run() # stdio transport
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()