bithuman-mcp 0.2.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,7 @@
1
+ # Required — your bitHuman API secret (https://www.bithuman.ai/#developer)
2
+ BITHUMAN_API_SECRET=your_api_secret_here
3
+
4
+ # Optional overrides
5
+ # BITHUMAN_API_BASE=https://api.bithuman.ai
6
+ # BITHUMAN_MCP_TRANSPORT=stdio # or: streamable-http
7
+ # BITHUMAN_MCP_TIMEOUT=120
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ .env
6
+ *.wav
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: bithuman-mcp
3
+ Version: 0.2.0
4
+ Summary: Model Context Protocol server for the bitHuman AI avatar platform
5
+ Project-URL: Homepage, https://bithuman.ai
6
+ Project-URL: Documentation, https://docs.bithuman.ai
7
+ Project-URL: Source, https://github.com/bithuman-product/bithuman-sdk-public/tree/main/mcp
8
+ Author-email: bitHuman <support@bithuman.ai>
9
+ License: Apache-2.0
10
+ Keywords: agent,ai,avatar,bithuman,mcp,model-context-protocol,tts
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: httpx>=0.27
16
+ Requires-Dist: mcp[cli]>=1.2.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # bitHuman MCP server
20
+
21
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
22
+ the [bitHuman](https://bithuman.ai) avatar platform as tools any MCP-capable AI
23
+ agent can call — Claude Desktop, Claude Code, Cursor, and others.
24
+
25
+ It's a thin, fully-documented wrapper over the public REST API
26
+ (`https://api.bithuman.ai`). Every tool maps to one documented endpoint; see the
27
+ [API docs](https://docs.bithuman.ai/api/overview) and the
28
+ [OpenAPI spec](https://docs.bithuman.ai/api/openapi.yaml).
29
+
30
+ ## Tools
31
+
32
+ | Tool | What it does |
33
+ |------|--------------|
34
+ | `validate_api_secret` | Check the API secret is valid (free). |
35
+ | `get_credit_balance` | Current credits, plan, and minutes estimate. |
36
+ | `get_usage` | Usage/metering history (paginated, date-filterable). |
37
+ | `list_voices` | Built-in (M1–M5 / F1–F5) and custom TTS voices. |
38
+ | `text_to_speech` | Synthesize speech → a WAV file. |
39
+ | `generate_agent` | Create an avatar agent from a prompt / image / video / audio. |
40
+ | `get_agent_status` | Poll agent generation progress. |
41
+ | `get_agent` | Fetch an existing agent's details. |
42
+ | `list_agents` | List your agents, newest first (paginated). |
43
+ | `update_agent_prompt` | Change an agent's system prompt. |
44
+ | `delete_agent` | Permanently delete an agent you own. |
45
+ | `agent_speak` | Make a live agent speak text in its active sessions. |
46
+ | `add_agent_context` | Silently inject knowledge into a live agent. |
47
+ | `get_dynamics` | List an agent's gesture animations. |
48
+ | `generate_dynamics` | Generate new gestures (wave, nod, laugh, idle…). |
49
+ | `create_embed_token` | Mint a 1-hour JWT to embed an agent on a website. |
50
+ | `upload_file` | Upload an asset (URL or local file) → CDN URL. |
51
+ | `create_webhook` / `list_webhooks` / `delete_webhook` / `test_webhook` | Manage signed event webhooks (agent.ready / agent.failed). |
52
+
53
+ ## Setup
54
+
55
+ You need an API secret from the [Developer Dashboard](https://www.bithuman.ai/#developer).
56
+
57
+ The package is self-contained. The easiest way to run it without installing is
58
+ with [`uvx`](https://docs.astral.sh/uv/) (recommended for MCP clients), or you
59
+ can `pip install` it.
60
+
61
+ ```bash
62
+ # one-off run from a checkout
63
+ cd mcp
64
+ pip install .
65
+ BITHUMAN_API_SECRET=sk_... bithuman-mcp
66
+ ```
67
+
68
+ ## Use with Claude Desktop / Claude Code
69
+
70
+ Add it to your MCP client config. For **Claude Code**:
71
+
72
+ ```bash
73
+ claude mcp add bithuman \
74
+ -e BITHUMAN_API_SECRET=sk_your_secret \
75
+ -- uvx --from /absolute/path/to/bithuman-sdk-public/mcp bithuman-mcp
76
+ ```
77
+
78
+ For **Claude Desktop** (`claude_desktop_config.json`) or any client that takes a
79
+ JSON server block:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "bithuman": {
85
+ "command": "uvx",
86
+ "args": ["--from", "/absolute/path/to/bithuman-sdk-public/mcp", "bithuman-mcp"],
87
+ "env": { "BITHUMAN_API_SECRET": "sk_your_secret" }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Once published to PyPI you'll be able to drop the local path:
94
+ `"args": ["bithuman-mcp"]` with `"command": "uvx"`.
95
+
96
+ ## Configuration
97
+
98
+ | Env var | Default | Purpose |
99
+ |---------|---------|---------|
100
+ | `BITHUMAN_API_SECRET` | _(required)_ | Your API secret. Never logged. |
101
+ | `BITHUMAN_API_BASE` | `https://api.bithuman.ai` | API origin. |
102
+ | `BITHUMAN_MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http`. |
103
+ | `BITHUMAN_MCP_TIMEOUT` | `120` | Per-request timeout (seconds). |
104
+
105
+ ## Notes
106
+
107
+ - **Async work**: `generate_agent` and `generate_dynamics` return immediately
108
+ with a `processing` status. Poll `get_agent_status` / `get_dynamics` until
109
+ `ready` (generation takes 2–5 minutes).
110
+ - **Credits**: `generate_agent` (~250 credits) and `text_to_speech` consume
111
+ credits. Check `get_credit_balance` first if cost matters.
112
+ - **Errors**: non-2xx responses come back as a structured `{error, status_code,
113
+ body, hint}` object. The error catalog is at
114
+ <https://docs.bithuman.ai/api/errors>.
115
+
116
+ ## License
117
+
118
+ Apache-2.0.
@@ -0,0 +1,100 @@
1
+ # bitHuman MCP server
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
4
+ the [bitHuman](https://bithuman.ai) avatar platform as tools any MCP-capable AI
5
+ agent can call — Claude Desktop, Claude Code, Cursor, and others.
6
+
7
+ It's a thin, fully-documented wrapper over the public REST API
8
+ (`https://api.bithuman.ai`). Every tool maps to one documented endpoint; see the
9
+ [API docs](https://docs.bithuman.ai/api/overview) and the
10
+ [OpenAPI spec](https://docs.bithuman.ai/api/openapi.yaml).
11
+
12
+ ## Tools
13
+
14
+ | Tool | What it does |
15
+ |------|--------------|
16
+ | `validate_api_secret` | Check the API secret is valid (free). |
17
+ | `get_credit_balance` | Current credits, plan, and minutes estimate. |
18
+ | `get_usage` | Usage/metering history (paginated, date-filterable). |
19
+ | `list_voices` | Built-in (M1–M5 / F1–F5) and custom TTS voices. |
20
+ | `text_to_speech` | Synthesize speech → a WAV file. |
21
+ | `generate_agent` | Create an avatar agent from a prompt / image / video / audio. |
22
+ | `get_agent_status` | Poll agent generation progress. |
23
+ | `get_agent` | Fetch an existing agent's details. |
24
+ | `list_agents` | List your agents, newest first (paginated). |
25
+ | `update_agent_prompt` | Change an agent's system prompt. |
26
+ | `delete_agent` | Permanently delete an agent you own. |
27
+ | `agent_speak` | Make a live agent speak text in its active sessions. |
28
+ | `add_agent_context` | Silently inject knowledge into a live agent. |
29
+ | `get_dynamics` | List an agent's gesture animations. |
30
+ | `generate_dynamics` | Generate new gestures (wave, nod, laugh, idle…). |
31
+ | `create_embed_token` | Mint a 1-hour JWT to embed an agent on a website. |
32
+ | `upload_file` | Upload an asset (URL or local file) → CDN URL. |
33
+ | `create_webhook` / `list_webhooks` / `delete_webhook` / `test_webhook` | Manage signed event webhooks (agent.ready / agent.failed). |
34
+
35
+ ## Setup
36
+
37
+ You need an API secret from the [Developer Dashboard](https://www.bithuman.ai/#developer).
38
+
39
+ The package is self-contained. The easiest way to run it without installing is
40
+ with [`uvx`](https://docs.astral.sh/uv/) (recommended for MCP clients), or you
41
+ can `pip install` it.
42
+
43
+ ```bash
44
+ # one-off run from a checkout
45
+ cd mcp
46
+ pip install .
47
+ BITHUMAN_API_SECRET=sk_... bithuman-mcp
48
+ ```
49
+
50
+ ## Use with Claude Desktop / Claude Code
51
+
52
+ Add it to your MCP client config. For **Claude Code**:
53
+
54
+ ```bash
55
+ claude mcp add bithuman \
56
+ -e BITHUMAN_API_SECRET=sk_your_secret \
57
+ -- uvx --from /absolute/path/to/bithuman-sdk-public/mcp bithuman-mcp
58
+ ```
59
+
60
+ For **Claude Desktop** (`claude_desktop_config.json`) or any client that takes a
61
+ JSON server block:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "bithuman": {
67
+ "command": "uvx",
68
+ "args": ["--from", "/absolute/path/to/bithuman-sdk-public/mcp", "bithuman-mcp"],
69
+ "env": { "BITHUMAN_API_SECRET": "sk_your_secret" }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Once published to PyPI you'll be able to drop the local path:
76
+ `"args": ["bithuman-mcp"]` with `"command": "uvx"`.
77
+
78
+ ## Configuration
79
+
80
+ | Env var | Default | Purpose |
81
+ |---------|---------|---------|
82
+ | `BITHUMAN_API_SECRET` | _(required)_ | Your API secret. Never logged. |
83
+ | `BITHUMAN_API_BASE` | `https://api.bithuman.ai` | API origin. |
84
+ | `BITHUMAN_MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http`. |
85
+ | `BITHUMAN_MCP_TIMEOUT` | `120` | Per-request timeout (seconds). |
86
+
87
+ ## Notes
88
+
89
+ - **Async work**: `generate_agent` and `generate_dynamics` return immediately
90
+ with a `processing` status. Poll `get_agent_status` / `get_dynamics` until
91
+ `ready` (generation takes 2–5 minutes).
92
+ - **Credits**: `generate_agent` (~250 credits) and `text_to_speech` consume
93
+ credits. Check `get_credit_balance` first if cost matters.
94
+ - **Errors**: non-2xx responses come back as a structured `{error, status_code,
95
+ body, hint}` object. The error catalog is at
96
+ <https://docs.bithuman.ai/api/errors>.
97
+
98
+ ## License
99
+
100
+ Apache-2.0.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "bithuman-mcp"
3
+ version = "0.2.0"
4
+ description = "Model Context Protocol server for the bitHuman AI avatar platform"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "Apache-2.0" }
8
+ authors = [{ name = "bitHuman", email = "support@bithuman.ai" }]
9
+ keywords = ["bithuman", "mcp", "model-context-protocol", "avatar", "ai", "tts", "agent"]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: Apache Software License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ dependencies = [
16
+ "mcp[cli]>=1.2.0",
17
+ "httpx>=0.27",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://bithuman.ai"
22
+ Documentation = "https://docs.bithuman.ai"
23
+ Source = "https://github.com/bithuman-product/bithuman-sdk-public/tree/main/mcp"
24
+
25
+ [project.scripts]
26
+ bithuman-mcp = "bithuman_mcp.server:main"
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/bithuman_mcp"]
@@ -0,0 +1,12 @@
1
+ """bitHuman MCP server — expose the bitHuman REST API as Model Context Protocol tools.
2
+
3
+ Lets any MCP-capable AI agent (Claude Desktop, Claude Code, Cursor, etc.)
4
+ discover and drive bitHuman: synthesize speech, generate avatar agents, make
5
+ them speak, manage gestures, mint embed tokens, and check credits.
6
+ """
7
+
8
+ __version__ = "0.2.0"
9
+
10
+ from .server import main
11
+
12
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,465 @@
1
+ """bitHuman MCP server.
2
+
3
+ A thin, well-documented Model Context Protocol wrapper over the bitHuman cloud
4
+ REST API (https://api.bithuman.ai). Every tool maps to one documented endpoint
5
+ (see https://docs.bithuman.ai/api/overview and the OpenAPI spec at
6
+ https://docs.bithuman.ai/api/openapi.yaml).
7
+
8
+ Auth: set BITHUMAN_API_SECRET in the environment (get one at
9
+ https://www.bithuman.ai/#developer). The server never logs or echoes it.
10
+
11
+ Transport: stdio by default (works with Claude Desktop / Claude Code / Cursor).
12
+ Set BITHUMAN_MCP_TRANSPORT=streamable-http to serve over HTTP instead.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import httpx
23
+ from mcp.server.fastmcp import FastMCP
24
+
25
+ API_BASE = os.environ.get("BITHUMAN_API_BASE", "https://api.bithuman.ai").rstrip("/")
26
+ API_SECRET = os.environ.get("BITHUMAN_API_SECRET", "")
27
+ TIMEOUT = float(os.environ.get("BITHUMAN_MCP_TIMEOUT", "120"))
28
+
29
+ mcp = FastMCP(
30
+ "bitHuman",
31
+ instructions=(
32
+ "Tools for the bitHuman real-time AI avatar platform. Use them to "
33
+ "synthesize speech, generate and manage avatar agents, drive live "
34
+ "sessions (speak / inject context / gestures), mint website embed "
35
+ "tokens, upload assets, and check credit balance. Avatars are keyed by "
36
+ "a short agent code like 'A91XMB7113'. Agent generation and dynamics "
37
+ "are async — poll the matching status tool until status is 'ready'. "
38
+ "Speech synthesis and agent generation consume credits; check the "
39
+ "balance first with get_credit_balance if cost matters."
40
+ ),
41
+ )
42
+
43
+
44
+ def _client() -> httpx.AsyncClient:
45
+ """A configured async HTTP client with the api-secret header attached."""
46
+ if not API_SECRET:
47
+ raise RuntimeError(
48
+ "BITHUMAN_API_SECRET is not set. Get one at "
49
+ "https://www.bithuman.ai/#developer and export it before starting "
50
+ "the MCP server."
51
+ )
52
+ return httpx.AsyncClient(
53
+ base_url=API_BASE,
54
+ headers={"api-secret": API_SECRET},
55
+ timeout=TIMEOUT,
56
+ )
57
+
58
+
59
+ def _json_or_text(resp: httpx.Response) -> Any:
60
+ """Return parsed JSON, or a structured error dict on non-2xx / non-JSON."""
61
+ try:
62
+ body: Any = resp.json()
63
+ except ValueError:
64
+ body = resp.text
65
+ if resp.status_code >= 400:
66
+ return {
67
+ "error": True,
68
+ "status_code": resp.status_code,
69
+ "body": body,
70
+ "hint": "See https://docs.bithuman.ai/api/errors for the error catalog.",
71
+ }
72
+ return body
73
+
74
+
75
+ # ──────────────────────────────────────────────────────────────────────────
76
+ # Authentication & account
77
+ # ──────────────────────────────────────────────────────────────────────────
78
+
79
+ @mcp.tool()
80
+ async def validate_api_secret() -> dict:
81
+ """Verify the configured bitHuman API secret is valid and the account active.
82
+
83
+ Cheapest possible check — does not consume credits. Returns {"valid": bool}.
84
+ Always call this first if you are unsure the credentials work.
85
+ """
86
+ async with _client() as c:
87
+ return _json_or_text(await c.post("/v1/validate"))
88
+
89
+
90
+ @mcp.tool()
91
+ async def get_credit_balance(user_id: str | None = None, app: str = "imaginex") -> dict:
92
+ """Check the account's current credit balance, plan, and estimated minutes.
93
+
94
+ Args:
95
+ user_id: Optional account UUID. Omit to use the API secret's own account.
96
+ app: App identifier for multi-app subscriptions (default "imaginex").
97
+
98
+ Returns balance, plan_credits, topup_credits, and a per-mode minutes_estimate.
99
+ Agent generation costs ~250 credits; speech and live minutes are metered.
100
+ """
101
+ params: dict[str, str] = {"app": app}
102
+ if user_id:
103
+ params["user_id"] = user_id
104
+ async with _client() as c:
105
+ return _json_or_text(await c.get("/v2/credit-summaries", params=params))
106
+
107
+
108
+ # ──────────────────────────────────────────────────────────────────────────
109
+ # Voice / text-to-speech
110
+ # ──────────────────────────────────────────────────────────────────────────
111
+
112
+ @mcp.tool()
113
+ async def list_voices() -> dict:
114
+ """List the built-in and custom TTS voices available to this account.
115
+
116
+ Built-in voices are M1–M5 (male) and F1–F5 (female). Use an id with
117
+ text_to_speech. Designed voices from the Voice Designer
118
+ (https://www.bithuman.ai/voice) are passed as a voice_code instead.
119
+ """
120
+ async with _client() as c:
121
+ return _json_or_text(await c.get("/v1/voices"))
122
+
123
+
124
+ @mcp.tool()
125
+ async def text_to_speech(
126
+ text: str,
127
+ output_path: str,
128
+ voice: str = "M1",
129
+ voice_code: str | None = None,
130
+ language: str = "en",
131
+ speed: float = 1.05,
132
+ total_steps: int = 8,
133
+ ) -> dict:
134
+ """Synthesize speech from text and save it as a WAV file. Consumes credits.
135
+
136
+ Args:
137
+ text: Text to speak (any length; multi-sentence supported).
138
+ output_path: Absolute path to write the resulting .wav file to.
139
+ voice: Built-in voice id (M1–M5, F1–F5). Ignored if voice_code is set.
140
+ voice_code: A designed-voice handle from the Voice Designer (UUID or
141
+ bv1_… code). Takes precedence over `voice`.
142
+ language: ISO-2 language code (31 languages supported).
143
+ speed: Playback rate, 0.7–2.0.
144
+ total_steps: Denoise steps — 5 fast, 8 balanced, 12 highest quality.
145
+
146
+ Returns the written file path and byte size. Read the WAV from output_path
147
+ to play or attach it.
148
+ """
149
+ payload: dict[str, Any] = {
150
+ "text": text,
151
+ "voice": voice,
152
+ "language": language,
153
+ "speed": speed,
154
+ "total_steps": total_steps,
155
+ "format": "wav",
156
+ }
157
+ if voice_code:
158
+ payload["voice_code"] = voice_code
159
+ async with _client() as c:
160
+ resp = await c.post("/v1/tts", json=payload)
161
+ if resp.status_code >= 400:
162
+ return _json_or_text(resp)
163
+ out = Path(output_path).expanduser()
164
+ out.parent.mkdir(parents=True, exist_ok=True)
165
+ out.write_bytes(resp.content)
166
+ return {"path": str(out), "bytes": len(resp.content), "format": "wav"}
167
+
168
+
169
+ # ──────────────────────────────────────────────────────────────────────────
170
+ # Agent generation & management
171
+ # ──────────────────────────────────────────────────────────────────────────
172
+
173
+ @mcp.tool()
174
+ async def generate_agent(
175
+ prompt: str | None = None,
176
+ image: str | None = None,
177
+ video: str | None = None,
178
+ audio: str | None = None,
179
+ aspect_ratio: str = "16:9",
180
+ ) -> dict:
181
+ """Create a new avatar agent. Async (2–5 min) and costs ~250 credits.
182
+
183
+ Args:
184
+ prompt: Personality / system prompt. A random default is used if omitted.
185
+ image: URL to a face image for appearance.
186
+ video: URL to a video for appearance and mannerisms.
187
+ audio: URL to audio for voice cloning.
188
+ aspect_ratio: "16:9", "9:16", or "1:1".
189
+
190
+ Returns an agent_id and status "processing". Poll get_agent_status(agent_id)
191
+ until status is "ready", then the agent is usable for embedding / live calls.
192
+ """
193
+ payload: dict[str, Any] = {"aspect_ratio": aspect_ratio}
194
+ for k, v in (("prompt", prompt), ("image", image), ("video", video), ("audio", audio)):
195
+ if v:
196
+ payload[k] = v
197
+ async with _client() as c:
198
+ return _json_or_text(await c.post("/v1/agent/generate", json=payload))
199
+
200
+
201
+ @mcp.tool()
202
+ async def get_agent_status(agent_id: str) -> dict:
203
+ """Poll the generation status of an agent created with generate_agent.
204
+
205
+ Returns the current status (processing / ready / failed) and progress.
206
+ """
207
+ async with _client() as c:
208
+ return _json_or_text(await c.get(f"/v1/agent/status/{agent_id}"))
209
+
210
+
211
+ @mcp.tool()
212
+ async def get_agent(code: str) -> dict:
213
+ """Retrieve full details for an existing agent by its code (e.g. 'A91XMB7113')."""
214
+ async with _client() as c:
215
+ return _json_or_text(await c.get(f"/v1/agent/{code}"))
216
+
217
+
218
+ @mcp.tool()
219
+ async def update_agent_prompt(code: str, system_prompt: str) -> dict:
220
+ """Update an existing agent's system prompt / personality.
221
+
222
+ The agent must already exist (create one with generate_agent first).
223
+ """
224
+ async with _client() as c:
225
+ return _json_or_text(
226
+ await c.post(f"/v1/agent/{code}", json={"system_prompt": system_prompt})
227
+ )
228
+
229
+
230
+ # ──────────────────────────────────────────────────────────────────────────
231
+ # Live-session control
232
+ # ──────────────────────────────────────────────────────────────────────────
233
+
234
+ @mcp.tool()
235
+ async def agent_speak(code: str, message: str, room_id: str | None = None) -> dict:
236
+ """Make a live agent speak the given text aloud in its active sessions.
237
+
238
+ The agent must already be in at least one active LiveKit room (e.g. a user
239
+ has an open embed/session). Returns how many rooms received the message.
240
+
241
+ Args:
242
+ code: Agent code.
243
+ message: Text for the avatar to speak.
244
+ room_id: Optional — target one room; defaults to all active rooms.
245
+ """
246
+ payload: dict[str, Any] = {"message": message}
247
+ if room_id:
248
+ payload["room_id"] = room_id
249
+ async with _client() as c:
250
+ return _json_or_text(await c.post(f"/v1/agent/{code}/speak", json=payload))
251
+
252
+
253
+ @mcp.tool()
254
+ async def add_agent_context(code: str, context: str, room_id: str | None = None) -> dict:
255
+ """Silently inject background knowledge into a live agent's context.
256
+
257
+ The avatar won't say this aloud but will use it in future responses
258
+ (e.g. "The customer just purchased a premium plan.").
259
+ """
260
+ payload: dict[str, Any] = {"context": context, "type": "add_context"}
261
+ if room_id:
262
+ payload["room_id"] = room_id
263
+ async with _client() as c:
264
+ return _json_or_text(await c.post(f"/v1/agent/{code}/add-context", json=payload))
265
+
266
+
267
+ # ──────────────────────────────────────────────────────────────────────────
268
+ # Dynamics (gesture animations)
269
+ # ──────────────────────────────────────────────────────────────────────────
270
+
271
+ @mcp.tool()
272
+ async def get_dynamics(agent_id: str) -> dict:
273
+ """List the gesture animations (wave, nod, laugh, idle…) available for an agent.
274
+
275
+ Returns a status and a map of gesture name → video URL.
276
+ """
277
+ async with _client() as c:
278
+ return _json_or_text(await c.get(f"/v1/dynamics/{agent_id}"))
279
+
280
+
281
+ @mcp.tool()
282
+ async def generate_dynamics(
283
+ agent_id: str, image_url: str | None = None, duration: int = 5, model: str = "seedance"
284
+ ) -> dict:
285
+ """Generate gesture animations for an agent. Async — poll get_dynamics to track.
286
+
287
+ Args:
288
+ agent_id: Agent code.
289
+ image_url: Source image; defaults to the agent's primary image if omitted.
290
+ duration: Gesture length in seconds.
291
+ model: "seedance" (default), "quality", "speed", or "auto".
292
+ """
293
+ payload: dict[str, Any] = {"agent_id": agent_id, "duration": duration, "model": model}
294
+ if image_url:
295
+ payload["image_url"] = image_url
296
+ async with _client() as c:
297
+ return _json_or_text(await c.post("/v1/dynamics/generate", json=payload))
298
+
299
+
300
+ # ──────────────────────────────────────────────────────────────────────────
301
+ # Embedding & files
302
+ # ──────────────────────────────────────────────────────────────────────────
303
+
304
+ @mcp.tool()
305
+ async def create_embed_token(agent_id: str, fingerprint: str) -> dict:
306
+ """Mint a short-lived (1 h) JWT to embed an agent on a website via iframe.
307
+
308
+ Call this from a backend — never expose the API secret to a browser. The
309
+ returned data.token goes in the embed widget's `data-token` attribute.
310
+
311
+ Args:
312
+ agent_id: Agent code to embed.
313
+ fingerprint: Stable per-device hex string for session tracking / rate
314
+ limiting (e.g. a hashed device id).
315
+ """
316
+ async with _client() as c:
317
+ return _json_or_text(
318
+ await c.post(
319
+ "/v1/embed-tokens/request",
320
+ json={"agent_id": agent_id, "fingerprint": fingerprint},
321
+ )
322
+ )
323
+
324
+
325
+ @mcp.tool()
326
+ async def upload_file(
327
+ file_url: str | None = None,
328
+ file_path: str | None = None,
329
+ file_type: str = "auto",
330
+ ) -> dict:
331
+ """Upload an image/video/audio/document and get back a public CDN URL.
332
+
333
+ Provide exactly one of file_url (download by URL) or file_path (a local file,
334
+ uploaded as base64). The returned data.file_url can be passed to
335
+ generate_agent's image/video/audio arguments.
336
+
337
+ Args:
338
+ file_url: Public URL to fetch the file from.
339
+ file_path: Absolute path to a local file to upload.
340
+ file_type: "auto" (default), "image", "video", "audio", or "document".
341
+ """
342
+ if bool(file_url) == bool(file_path):
343
+ return {"error": True, "message": "Provide exactly one of file_url or file_path."}
344
+ if file_url:
345
+ payload: dict[str, Any] = {"file_url": file_url, "file_type": file_type}
346
+ else:
347
+ p = Path(file_path).expanduser() # type: ignore[arg-type]
348
+ if not p.is_file():
349
+ return {"error": True, "message": f"No such file: {p}"}
350
+ payload = {
351
+ "file_data": base64.b64encode(p.read_bytes()).decode("ascii"),
352
+ "file_name": p.name,
353
+ "file_type": file_type,
354
+ }
355
+ async with _client() as c:
356
+ return _json_or_text(await c.post("/v1/files/upload", json=payload))
357
+
358
+
359
+ @mcp.tool()
360
+ async def list_agents(limit: int = 20, offset: int = 0, status: str | None = None) -> dict:
361
+ """List the avatar agents owned by this account, newest first (paginated).
362
+
363
+ Args:
364
+ limit: Page size (1–100).
365
+ offset: Number of agents to skip.
366
+ status: Optional generation-state filter (e.g. ready, processing, failed).
367
+
368
+ Returns {data: [...], pagination: {limit, offset, total, has_more}}.
369
+ """
370
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
371
+ if status:
372
+ params["status"] = status
373
+ async with _client() as c:
374
+ return _json_or_text(await c.get("/v1/agents", params=params))
375
+
376
+
377
+ @mcp.tool()
378
+ async def delete_agent(code: str) -> dict:
379
+ """Permanently delete an agent you own (by code). Usage history is retained."""
380
+ async with _client() as c:
381
+ return _json_or_text(await c.delete(f"/v1/agent/{code}"))
382
+
383
+
384
+ @mcp.tool()
385
+ async def get_usage(
386
+ limit: int = 50,
387
+ offset: int = 0,
388
+ start: str | None = None,
389
+ end: str | None = None,
390
+ agent_code: str | None = None,
391
+ ) -> dict:
392
+ """Return this account's usage/metering history, newest first (paginated).
393
+
394
+ Args:
395
+ limit: Page size (1–200).
396
+ offset: Rows to skip.
397
+ start: ISO-8601 timestamp — only events at/after this time.
398
+ end: ISO-8601 timestamp — only events at/before this time.
399
+ agent_code: Only events for this agent.
400
+
401
+ Each row has activity_type, pricing_code, agent_code, credits_change
402
+ (signed; usage is positive credits consumed), and created_at.
403
+ """
404
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
405
+ for k, v in (("start", start), ("end", end), ("agent_code", agent_code)):
406
+ if v:
407
+ params[k] = v
408
+ async with _client() as c:
409
+ return _json_or_text(await c.get("/v1/usage", params=params))
410
+
411
+
412
+ @mcp.tool()
413
+ async def create_webhook(
414
+ url: str, events: list[str] | None = None, description: str | None = None
415
+ ) -> dict:
416
+ """Register a webhook to receive signed event notifications.
417
+
418
+ Args:
419
+ url: HTTPS endpoint to deliver events to.
420
+ events: Event types to subscribe to (agent.ready, agent.failed). Omit
421
+ for all.
422
+ description: Optional label.
423
+
424
+ The response includes a one-time `secret` (store it — it signs the
425
+ X-BitHuman-Signature header and is never returned again).
426
+ """
427
+ payload: dict[str, Any] = {"url": url, "events": events or []}
428
+ if description:
429
+ payload["description"] = description
430
+ async with _client() as c:
431
+ return _json_or_text(await c.post("/v1/webhooks", json=payload))
432
+
433
+
434
+ @mcp.tool()
435
+ async def list_webhooks() -> dict:
436
+ """List this account's registered webhooks (signing secrets redacted)."""
437
+ async with _client() as c:
438
+ return _json_or_text(await c.get("/v1/webhooks"))
439
+
440
+
441
+ @mcp.tool()
442
+ async def delete_webhook(webhook_id: str) -> dict:
443
+ """Delete a webhook by id."""
444
+ async with _client() as c:
445
+ return _json_or_text(await c.delete(f"/v1/webhooks/{webhook_id}"))
446
+
447
+
448
+ @mcp.tool()
449
+ async def test_webhook(webhook_id: str) -> dict:
450
+ """Send a one-off `ping` event to a webhook to confirm it's reachable.
451
+
452
+ Returns {delivered, status_code, attempts}.
453
+ """
454
+ async with _client() as c:
455
+ return _json_or_text(await c.post(f"/v1/webhooks/{webhook_id}/test"))
456
+
457
+
458
+ def main() -> None:
459
+ """Entry point. Serves over stdio (default) or streamable-http."""
460
+ transport = os.environ.get("BITHUMAN_MCP_TRANSPORT", "stdio")
461
+ mcp.run(transport=transport) # type: ignore[arg-type]
462
+
463
+
464
+ if __name__ == "__main__":
465
+ main()