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,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()
|