liveface-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.
- liveface_mcp-0.1.0/.gitignore +4 -0
- liveface_mcp-0.1.0/LICENSE +30 -0
- liveface_mcp-0.1.0/PKG-INFO +135 -0
- liveface_mcp-0.1.0/PYPI.md +89 -0
- liveface_mcp-0.1.0/README.md +114 -0
- liveface_mcp-0.1.0/build.sh +16 -0
- liveface_mcp-0.1.0/e2e/README.md +29 -0
- liveface_mcp-0.1.0/e2e/harness.py +130 -0
- liveface_mcp-0.1.0/e2e/studio.e2e.mjs +130 -0
- liveface_mcp-0.1.0/liveface_mcp/__init__.py +0 -0
- liveface_mcp-0.1.0/liveface_mcp/account.py +236 -0
- liveface_mcp-0.1.0/liveface_mcp/client.py +146 -0
- liveface_mcp-0.1.0/liveface_mcp/config.py +74 -0
- liveface_mcp-0.1.0/liveface_mcp/creation.py +240 -0
- liveface_mcp-0.1.0/liveface_mcp/server.py +41 -0
- liveface_mcp-0.1.0/liveface_mcp/services.py +45 -0
- liveface_mcp-0.1.0/liveface_mcp/tools/__init__.py +16 -0
- liveface_mcp-0.1.0/liveface_mcp/tools/account.py +50 -0
- liveface_mcp-0.1.0/liveface_mcp/tools/speak.py +68 -0
- liveface_mcp-0.1.0/liveface_mcp/tools/studio.py +359 -0
- liveface_mcp-0.1.0/liveface_mcp/transport.py +98 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/app.ts +51 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/assets.d.ts +4 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/bridge.ts +153 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/dom.ts +26 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/main.ts +37 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/photo.ts +27 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/pictures.ts +47 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/recorder.ts +105 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/stage.ts +53 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/types.ts +88 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/ui.ts +51 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/views/connect.ts +69 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/views/create.ts +340 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/views/library.ts +251 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio/wording.ts +40 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio.html +58 -0
- liveface_mcp-0.1.0/liveface_mcp/web/studio.js +21 -0
- liveface_mcp-0.1.0/liveface_mcp/web/window.entry.ts +165 -0
- liveface_mcp-0.1.0/liveface_mcp/web/window.html +19 -0
- liveface_mcp-0.1.0/liveface_mcp/web/window.js +21 -0
- liveface_mcp-0.1.0/liveface_mcp/window.py +194 -0
- liveface_mcp-0.1.0/pyproject.toml +49 -0
- liveface_mcp-0.1.0/tests/__init__.py +0 -0
- liveface_mcp-0.1.0/tests/fakes.py +46 -0
- liveface_mcp-0.1.0/tests/test_account.py +141 -0
- liveface_mcp-0.1.0/tests/test_config.py +44 -0
- liveface_mcp-0.1.0/tests/test_creation.py +230 -0
- liveface_mcp-0.1.0/tests/test_studio.py +238 -0
- liveface_mcp-0.1.0/tests/test_transport.py +83 -0
- liveface_mcp-0.1.0/tests/test_window.py +59 -0
- liveface_mcp-0.1.0/tsconfig.json +16 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Liveface MCP Server Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liveface (https://liveface.io). All rights reserved.
|
|
4
|
+
|
|
5
|
+
This package, liveface-mcp, including its code, pages and documentation
|
|
6
|
+
(the "Software"), is licensed, not sold.
|
|
7
|
+
|
|
8
|
+
1. Use. You may install the Software and run it, unmodified, with AI agent
|
|
9
|
+
applications to use avatars from a Liveface account, for as long as your
|
|
10
|
+
use of Liveface follows Liveface's terms of service.
|
|
11
|
+
|
|
12
|
+
2. Distribution. You may not copy, publish, sell, sublicense or distribute
|
|
13
|
+
the Software or a modified copy of it, on its own or as part of another
|
|
14
|
+
library, tool or service; point others to its published package instead.
|
|
15
|
+
|
|
16
|
+
3. Restrictions. You may not modify the Software, use it or any part of it
|
|
17
|
+
with a service other than Liveface, or remove this notice from it.
|
|
18
|
+
|
|
19
|
+
4. Ownership. Liveface keeps all rights in the Software not expressly
|
|
20
|
+
granted here.
|
|
21
|
+
|
|
22
|
+
5. Termination. This licence ends automatically if you break its terms or
|
|
23
|
+
your Liveface account is closed; then you must stop using the Software.
|
|
24
|
+
|
|
25
|
+
6. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
26
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
|
|
28
|
+
LIVEFACE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
29
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
30
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN IT.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: liveface-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Speak as a Liveface avatar from Claude Code, Claude Desktop or Codex
|
|
5
|
+
Project-URL: Homepage, https://liveface.io
|
|
6
|
+
Author: Liveface
|
|
7
|
+
License: Liveface MCP Server Licence
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Liveface (https://liveface.io). All rights reserved.
|
|
10
|
+
|
|
11
|
+
This package, liveface-mcp, including its code, pages and documentation
|
|
12
|
+
(the "Software"), is licensed, not sold.
|
|
13
|
+
|
|
14
|
+
1. Use. You may install the Software and run it, unmodified, with AI agent
|
|
15
|
+
applications to use avatars from a Liveface account, for as long as your
|
|
16
|
+
use of Liveface follows Liveface's terms of service.
|
|
17
|
+
|
|
18
|
+
2. Distribution. You may not copy, publish, sell, sublicense or distribute
|
|
19
|
+
the Software or a modified copy of it, on its own or as part of another
|
|
20
|
+
library, tool or service; point others to its published package instead.
|
|
21
|
+
|
|
22
|
+
3. Restrictions. You may not modify the Software, use it or any part of it
|
|
23
|
+
with a service other than Liveface, or remove this notice from it.
|
|
24
|
+
|
|
25
|
+
4. Ownership. Liveface keeps all rights in the Software not expressly
|
|
26
|
+
granted here.
|
|
27
|
+
|
|
28
|
+
5. Termination. This licence ends automatically if you break its terms or
|
|
29
|
+
your Liveface account is closed; then you must stop using the Software.
|
|
30
|
+
|
|
31
|
+
6. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
32
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
|
|
34
|
+
LIVEFACE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
35
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
36
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN IT.
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Keywords: avatar,claude,codex,liveface,mcp,talking-avatar
|
|
39
|
+
Classifier: License :: Other/Proprietary License
|
|
40
|
+
Classifier: Operating System :: OS Independent
|
|
41
|
+
Classifier: Programming Language :: Python :: 3
|
|
42
|
+
Classifier: Topic :: Multimedia :: Video
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Requires-Dist: mcp<3,>=2
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# liveface-mcp: Liveface for AI agents
|
|
48
|
+
|
|
49
|
+
An MCP server for Claude Code, Claude Desktop and Codex. Inside the chat, the
|
|
50
|
+
**studio** lets you choose an avatar and a voice, write a line and get a video
|
|
51
|
+
of the avatar saying it, or make a new avatar from a photo, once you have
|
|
52
|
+
connected your [Liveface](https://liveface.io) account. Without an account,
|
|
53
|
+
shared avatars still speak in a browser window.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
With [uv](https://docs.astral.sh/uv/) nothing needs installing first. In
|
|
58
|
+
Claude Code:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
claude mcp add liveface -- uvx liveface-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or in the MCP config of Claude Desktop or Codex:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"liveface": {
|
|
70
|
+
"command": "uvx",
|
|
71
|
+
"args": ["liveface-mcp"],
|
|
72
|
+
"env": { "LIVEFACE_CLIENT_NAME": "Claude Desktop" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or install it with `pipx install liveface-mcp` (or `pip install liveface-mcp`)
|
|
79
|
+
and use the `liveface-mcp` command. Python 3.10 or later.
|
|
80
|
+
|
|
81
|
+
## Connecting your account
|
|
82
|
+
|
|
83
|
+
Ask for the studio and press **Connect your account** (or, in a terminal,
|
|
84
|
+
ask the agent to `connect_account`). A page of the dashboard opens with a code;
|
|
85
|
+
sign in as usual, check the code is the one the app shows, and press
|
|
86
|
+
**Approve**. That is the device sign-in of `docs/connected-apps.md`: the app
|
|
87
|
+
never sees your password, gets a session limited to avatars, their creation
|
|
88
|
+
and speech (never members, keys, deleting, or settings), keeps it in
|
|
89
|
+
`~/.liveface/mcp-credentials.json` (readable by you only), and is cut off at
|
|
90
|
+
once when you disconnect it in the dashboard's Settings, under Connected apps.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
| Variable | Meaning |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `LIVEFACE_BASE` | The site. Default `https://liveface.io`. |
|
|
97
|
+
| `LIVEFACE_CLIENT_NAME` | How this app names itself when asking to connect. |
|
|
98
|
+
| `LIVEFACE_CREDENTIALS` | Where the session is kept. Default `~/.liveface/mcp-credentials.json`. |
|
|
99
|
+
| `LIVEFACE_AVATARS` | Shared avatars, no account needed: `name=token` pairs (a share link's token). |
|
|
100
|
+
|
|
101
|
+
## The studio (an MCP app)
|
|
102
|
+
|
|
103
|
+
`avatar_studio()` returns a page the host draws inside the chat
|
|
104
|
+
(`ui://liveface/studio.html`, `text/html;profile=mcp-app`):
|
|
105
|
+
|
|
106
|
+
- **Library.** Your avatars as cards (and the shared ones), a voice (every
|
|
107
|
+
server voice, grouped by provider), the line (`[happy]`, `[sad]`... tags
|
|
108
|
+
change the face), **Create video**: the page runs the avatar engine, has the
|
|
109
|
+
avatar say it and records picture and voice into an MP4 (WebM where the
|
|
110
|
+
browser writes nothing else) to play or download. A browser voice cannot be
|
|
111
|
+
recorded, so an avatar on one is said in a server voice, and the page says
|
|
112
|
+
which.
|
|
113
|
+
- **New avatar.** A person (woman, man) or an animal, the photo, the style
|
|
114
|
+
(the dashboard's own pictures), the pose, the image service, the voice
|
|
115
|
+
(one that suits the person), a name, and the agreements in the dashboard's
|
|
116
|
+
own words. The wizard's steps run on this server (`creation.py`), with
|
|
117
|
+
progress; the new avatar is then chosen in the library. When its face's
|
|
118
|
+
points cannot be found (the dashboard has the person place them by hand),
|
|
119
|
+
the picture is made once more, then **Finish in the dashboard** opens the
|
|
120
|
+
wizard where the avatar is waiting.
|
|
121
|
+
- The page holds no account token: it asks this server through the host, and
|
|
122
|
+
the server calls the site. Its CSP lets it reach the site only.
|
|
123
|
+
- It needs a host that draws MCP apps. Elsewhere the tools still answer in
|
|
124
|
+
text, and `speak` opens its browser window.
|
|
125
|
+
|
|
126
|
+
## Tools
|
|
127
|
+
|
|
128
|
+
The model's: `avatar_studio`, `connect_account`, `account_status`,
|
|
129
|
+
`disconnect_account`, `list_avatars`, `speak(text, avatar)`, `stop`.
|
|
130
|
+
The page's own (hidden from the model): `studio_*`.
|
|
131
|
+
|
|
132
|
+
## Licence
|
|
133
|
+
|
|
134
|
+
Proprietary: free to use with your own Liveface account. See the `LICENSE`
|
|
135
|
+
file in the package.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# liveface-mcp: Liveface for AI agents
|
|
2
|
+
|
|
3
|
+
An MCP server for Claude Code, Claude Desktop and Codex. Inside the chat, the
|
|
4
|
+
**studio** lets you choose an avatar and a voice, write a line and get a video
|
|
5
|
+
of the avatar saying it, or make a new avatar from a photo, once you have
|
|
6
|
+
connected your [Liveface](https://liveface.io) account. Without an account,
|
|
7
|
+
shared avatars still speak in a browser window.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
With [uv](https://docs.astral.sh/uv/) nothing needs installing first. In
|
|
12
|
+
Claude Code:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
claude mcp add liveface -- uvx liveface-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
or in the MCP config of Claude Desktop or Codex:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"liveface": {
|
|
24
|
+
"command": "uvx",
|
|
25
|
+
"args": ["liveface-mcp"],
|
|
26
|
+
"env": { "LIVEFACE_CLIENT_NAME": "Claude Desktop" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install it with `pipx install liveface-mcp` (or `pip install liveface-mcp`)
|
|
33
|
+
and use the `liveface-mcp` command. Python 3.10 or later.
|
|
34
|
+
|
|
35
|
+
## Connecting your account
|
|
36
|
+
|
|
37
|
+
Ask for the studio and press **Connect your account** (or, in a terminal,
|
|
38
|
+
ask the agent to `connect_account`). A page of the dashboard opens with a code;
|
|
39
|
+
sign in as usual, check the code is the one the app shows, and press
|
|
40
|
+
**Approve**. That is the device sign-in of `docs/connected-apps.md`: the app
|
|
41
|
+
never sees your password, gets a session limited to avatars, their creation
|
|
42
|
+
and speech (never members, keys, deleting, or settings), keeps it in
|
|
43
|
+
`~/.liveface/mcp-credentials.json` (readable by you only), and is cut off at
|
|
44
|
+
once when you disconnect it in the dashboard's Settings, under Connected apps.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
| Variable | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `LIVEFACE_BASE` | The site. Default `https://liveface.io`. |
|
|
51
|
+
| `LIVEFACE_CLIENT_NAME` | How this app names itself when asking to connect. |
|
|
52
|
+
| `LIVEFACE_CREDENTIALS` | Where the session is kept. Default `~/.liveface/mcp-credentials.json`. |
|
|
53
|
+
| `LIVEFACE_AVATARS` | Shared avatars, no account needed: `name=token` pairs (a share link's token). |
|
|
54
|
+
|
|
55
|
+
## The studio (an MCP app)
|
|
56
|
+
|
|
57
|
+
`avatar_studio()` returns a page the host draws inside the chat
|
|
58
|
+
(`ui://liveface/studio.html`, `text/html;profile=mcp-app`):
|
|
59
|
+
|
|
60
|
+
- **Library.** Your avatars as cards (and the shared ones), a voice (every
|
|
61
|
+
server voice, grouped by provider), the line (`[happy]`, `[sad]`... tags
|
|
62
|
+
change the face), **Create video**: the page runs the avatar engine, has the
|
|
63
|
+
avatar say it and records picture and voice into an MP4 (WebM where the
|
|
64
|
+
browser writes nothing else) to play or download. A browser voice cannot be
|
|
65
|
+
recorded, so an avatar on one is said in a server voice, and the page says
|
|
66
|
+
which.
|
|
67
|
+
- **New avatar.** A person (woman, man) or an animal, the photo, the style
|
|
68
|
+
(the dashboard's own pictures), the pose, the image service, the voice
|
|
69
|
+
(one that suits the person), a name, and the agreements in the dashboard's
|
|
70
|
+
own words. The wizard's steps run on this server (`creation.py`), with
|
|
71
|
+
progress; the new avatar is then chosen in the library. When its face's
|
|
72
|
+
points cannot be found (the dashboard has the person place them by hand),
|
|
73
|
+
the picture is made once more, then **Finish in the dashboard** opens the
|
|
74
|
+
wizard where the avatar is waiting.
|
|
75
|
+
- The page holds no account token: it asks this server through the host, and
|
|
76
|
+
the server calls the site. Its CSP lets it reach the site only.
|
|
77
|
+
- It needs a host that draws MCP apps. Elsewhere the tools still answer in
|
|
78
|
+
text, and `speak` opens its browser window.
|
|
79
|
+
|
|
80
|
+
## Tools
|
|
81
|
+
|
|
82
|
+
The model's: `avatar_studio`, `connect_account`, `account_status`,
|
|
83
|
+
`disconnect_account`, `list_avatars`, `speak(text, avatar)`, `stop`.
|
|
84
|
+
The page's own (hidden from the model): `studio_*`.
|
|
85
|
+
|
|
86
|
+
## Licence
|
|
87
|
+
|
|
88
|
+
Proprietary: free to use with your own Liveface account. See the `LICENSE`
|
|
89
|
+
file in the package.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Liveface for AI agents
|
|
2
|
+
|
|
3
|
+
An MCP server for Claude Code, Claude Desktop and Codex. Inside the chat, the
|
|
4
|
+
**studio** lets you choose an avatar and a voice, write a line and get a video
|
|
5
|
+
of the avatar saying it, or make a new avatar from a photo, once you have
|
|
6
|
+
connected your Liveface account. Without an account, shared avatars still
|
|
7
|
+
speak in a browser window.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
From PyPI, for anyone: see [PYPI.md](PYPI.md) (`uvx liveface-mcp`), the page
|
|
12
|
+
PyPI shows. From this repository, to work on it:
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm ci --prefix ../embed && npm ci --prefix ../frontend # the build imports both
|
|
18
|
+
./build.sh # type-checks and bundles the two pages
|
|
19
|
+
uv venv .venv && VIRTUAL_ENV=.venv uv pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Register it. In Claude Code:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
claude mcp add liveface -- /abs/path/to/mcp/.venv/bin/liveface-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or in the MCP config of Claude Desktop or Codex:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"liveface": {
|
|
34
|
+
"command": "/abs/path/to/mcp/.venv/bin/liveface-mcp",
|
|
35
|
+
"env": { "LIVEFACE_CLIENT_NAME": "Claude Desktop" }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Connecting your account
|
|
42
|
+
|
|
43
|
+
Ask for the studio and press **Connect your account** (or, in a terminal,
|
|
44
|
+
ask the agent to `connect_account`). A page of the dashboard opens with a code;
|
|
45
|
+
sign in as usual, check the code is the one the app shows, and press
|
|
46
|
+
**Approve**. That is the device sign-in of `docs/connected-apps.md`: the app
|
|
47
|
+
never sees your password, gets a session limited to avatars, their creation
|
|
48
|
+
and speech (never members, keys, deleting, or settings), keeps it in
|
|
49
|
+
`~/.liveface/mcp-credentials.json` (readable by you only), and is cut off at
|
|
50
|
+
once when you disconnect it in the dashboard's Settings, under Connected apps.
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
| Variable | Meaning |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `LIVEFACE_BASE` | The site. Default `https://liveface.io`. |
|
|
57
|
+
| `LIVEFACE_CLIENT_NAME` | How this app names itself when asking to connect. |
|
|
58
|
+
| `LIVEFACE_CREDENTIALS` | Where the session is kept. Default `~/.liveface/mcp-credentials.json`. |
|
|
59
|
+
| `LIVEFACE_AVATARS` | Shared avatars, no account needed: `name=token` pairs (a share link's token). |
|
|
60
|
+
|
|
61
|
+
## The studio (an MCP app)
|
|
62
|
+
|
|
63
|
+
`avatar_studio()` returns a page the host draws inside the chat
|
|
64
|
+
(`ui://liveface/studio.html`, `text/html;profile=mcp-app`):
|
|
65
|
+
|
|
66
|
+
- **Library.** Your avatars as cards (and the shared ones), a voice (every
|
|
67
|
+
server voice, grouped by provider), the line (`[happy]`, `[sad]`... tags
|
|
68
|
+
change the face), **Create video**: the page runs the avatar engine, has the
|
|
69
|
+
avatar say it and records picture and voice into an MP4 (WebM where the
|
|
70
|
+
browser writes nothing else) to play or download. A browser voice cannot be
|
|
71
|
+
recorded, so an avatar on one is said in a server voice, and the page says
|
|
72
|
+
which.
|
|
73
|
+
- **New avatar.** A person (woman, man) or an animal, the photo, the style
|
|
74
|
+
(the dashboard's own pictures), the pose, the image service, the voice
|
|
75
|
+
(one that suits the person), a name, and the agreements in the dashboard's
|
|
76
|
+
own words. The wizard's steps run on this server (`creation.py`), with
|
|
77
|
+
progress; the new avatar is then chosen in the library. When its face's
|
|
78
|
+
points cannot be found (the dashboard has the person place them by hand),
|
|
79
|
+
the picture is made once more, then **Finish in the dashboard** opens the
|
|
80
|
+
wizard where the avatar is waiting.
|
|
81
|
+
- The page holds no account token: it asks this server through the host, and
|
|
82
|
+
the server calls the site. Its CSP lets it reach the site only.
|
|
83
|
+
- It needs a host that draws MCP apps. Elsewhere the tools still answer in
|
|
84
|
+
text, and `speak` opens its browser window.
|
|
85
|
+
|
|
86
|
+
## Tools
|
|
87
|
+
|
|
88
|
+
The model's: `avatar_studio`, `connect_account`, `account_status`,
|
|
89
|
+
`disconnect_account`, `list_avatars`, `speak(text, avatar)`, `stop`.
|
|
90
|
+
The page's own (hidden from the model): `studio_*`.
|
|
91
|
+
|
|
92
|
+
## Layout
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
liveface_mcp/
|
|
96
|
+
server.py wires the tools to the shared Services
|
|
97
|
+
services.py config, account, APIs, window, runs
|
|
98
|
+
config.py the environment
|
|
99
|
+
transport.py JSON and multipart over the standard library; ApiError
|
|
100
|
+
account.py the device sign-in, the session on disk, refreshing it
|
|
101
|
+
client.py the account's API, and the public share API
|
|
102
|
+
creation.py the wizard's steps, in the background, with progress
|
|
103
|
+
window.py the browser window for `speak`
|
|
104
|
+
tools/ speak, account, studio
|
|
105
|
+
web/ window.html + window.entry.ts; studio.html + studio/ (the page's modules)
|
|
106
|
+
tests/ unittest, over a fake site (fakes.py); no network
|
|
107
|
+
e2e/ the studio end to end in a real Chrome (README there)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Tests
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
python -m unittest discover -s tests -t .
|
|
114
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Builds the two pages this server serves: the avatar window (`speak`) and
|
|
3
|
+
# the studio (an MCP app). Run from anywhere; needs the repo's embed and
|
|
4
|
+
# frontend deps installed (npm ci in embed/ and frontend/), because the
|
|
5
|
+
# engine (embed/src) and the consent wording (frontend/src/i18n) are imported
|
|
6
|
+
# at build time.
|
|
7
|
+
set -eu
|
|
8
|
+
here=$(cd "$(dirname "$0")" && pwd)
|
|
9
|
+
web="$here/liveface_mcp/web"
|
|
10
|
+
cd "$here/../embed"
|
|
11
|
+
# Type-check the pages first (mcp/tsconfig.json), with the repo's TypeScript.
|
|
12
|
+
npx tsc -p "$here/tsconfig.json"
|
|
13
|
+
npx esbuild "$web/window.entry.ts" --bundle --format=iife --minify --target=es2020 --outfile="$web/window.js"
|
|
14
|
+
npx esbuild "$web/studio/main.ts" --bundle --format=iife --minify --target=es2020 \
|
|
15
|
+
--tsconfig="$here/../frontend/tsconfig.json" --loader:.webp=dataurl --outfile="$web/studio.js"
|
|
16
|
+
echo "built $web/window.js and $web/studio.js"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# The studio, end to end
|
|
2
|
+
|
|
3
|
+
A real Chrome drives the studio inside a mock MCP host (`harness.py`: the
|
|
4
|
+
real MCP server in-process, the page in a sandboxed iframe under the CSP its
|
|
5
|
+
resource declares, `/api/*` proxied to the backend as production's proxy
|
|
6
|
+
does), and the dashboard's `/connect` page:
|
|
7
|
+
|
|
8
|
+
1. the studio opens, not connected; **Connect your account** shows a code and
|
|
9
|
+
asks the host to open the dashboard;
|
|
10
|
+
2. the dashboard: sign in (it comes back to `/connect?code=`), check, Approve;
|
|
11
|
+
3. the studio connects and lists the account's avatars;
|
|
12
|
+
4. a video with the first one (its sound and length checked);
|
|
13
|
+
5. a new avatar from a photo: a man, 3D realistic, arms crossed;
|
|
14
|
+
6. a video with it.
|
|
15
|
+
|
|
16
|
+
It spends real image calls (step 5). Run it against a scratch copy of a local
|
|
17
|
+
database, never production:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# a backend of this checkout on :7003 (RIG_MODEL_PATH, KOKORO_*, a Gemini key in its database,
|
|
21
|
+
# APP_BASE_URL=http://localhost:5183, PUBLIC_BASE_URL=http://localhost:9099/api),
|
|
22
|
+
# its dashboard on :5183 proxying /api to :7003, then:
|
|
23
|
+
cd mcp
|
|
24
|
+
E2E_BACKEND=http://localhost:7003 LIVEFACE_BASE=http://localhost:9099 \
|
|
25
|
+
LIVEFACE_CREDENTIALS=/tmp/e2e-creds.json python e2e/harness.py &
|
|
26
|
+
E2E_USER=... E2E_PASSWORD=... node e2e/studio.e2e.mjs /path/to/photo.jpg # skip-create: steps 1-4
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Screenshots and the videos land in `e2e/out/` (ignored).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""A mock MCP host for the studio, end to end: the real MCP server in this
|
|
2
|
+
process (its tools called as a host calls them), the studio page in a
|
|
3
|
+
sandboxed iframe under the CSP its resource declares, and /api/* proxied to
|
|
4
|
+
the backend the way production's proxy lays it out.
|
|
5
|
+
|
|
6
|
+
E2E_BACKEND=http://localhost:7003 LIVEFACE_BASE=http://localhost:9099 python e2e/harness.py
|
|
7
|
+
|
|
8
|
+
(run from mcp/, so `liveface_mcp` is this checkout's; e2e/README.md)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import threading
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
|
|
19
|
+
from liveface_mcp import config, server
|
|
20
|
+
from liveface_mcp.services import Services
|
|
21
|
+
from liveface_mcp.tools import studio
|
|
22
|
+
|
|
23
|
+
BACKEND = os.environ.get("E2E_BACKEND", "http://localhost:7003")
|
|
24
|
+
PORT = int(os.environ.get("E2E_PORT", "9099"))
|
|
25
|
+
|
|
26
|
+
services = Services.of(config.load())
|
|
27
|
+
mcp = server.build(services)
|
|
28
|
+
loop = asyncio.new_event_loop()
|
|
29
|
+
threading.Thread(target=loop.run_forever, daemon=True).start()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run(coro):
|
|
33
|
+
return asyncio.run_coroutine_threadsafe(coro, loop).result(timeout=300)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def csp() -> str:
|
|
37
|
+
[resource] = [r for r in run(mcp.list_resources()) if str(r.uri) == studio.URI]
|
|
38
|
+
declared = resource.meta["ui"]["csp"]
|
|
39
|
+
connect = " ".join(declared["connectDomains"])
|
|
40
|
+
resources = " ".join(declared["resourceDomains"])
|
|
41
|
+
# blob: in connect-src is the TEST's only (it reads the video back); the page does not need it.
|
|
42
|
+
return (
|
|
43
|
+
"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; "
|
|
44
|
+
f"img-src data: blob: {resources}; media-src data: blob: {resources}; connect-src {connect} blob:"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
HOST = """<!doctype html><title>mock host</title>
|
|
49
|
+
<iframe id="app" sandbox="allow-scripts allow-downloads" src="/studio.html" style="width:560px;height:1000px;border:1px solid #888"></iframe>
|
|
50
|
+
<script>
|
|
51
|
+
const frame = document.getElementById("app");
|
|
52
|
+
window.__log = []; window.__opened = [];
|
|
53
|
+
addEventListener("message", async (e) => {
|
|
54
|
+
if (e.source !== frame.contentWindow) return;
|
|
55
|
+
const m = e.data; window.__log.push(m.method || "response");
|
|
56
|
+
const reply = (result) => frame.contentWindow.postMessage({ jsonrpc: "2.0", id: m.id, result }, "*");
|
|
57
|
+
if (m.method === "ui/initialize") reply({ protocolVersion: "2026-01-26", hostInfo: { name: "mock", version: "1" }, hostCapabilities: {}, hostContext: {} });
|
|
58
|
+
else if (m.method === "ui/notifications/initialized") {
|
|
59
|
+
const opened = await (await fetch("/tool", { method: "POST", body: JSON.stringify({ name: "avatar_studio", arguments: {} }) })).json();
|
|
60
|
+
frame.contentWindow.postMessage({ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: opened }, "*");
|
|
61
|
+
} else if (m.method === "tools/call") {
|
|
62
|
+
reply(await (await fetch("/tool", { method: "POST", body: JSON.stringify(m.params) })).json());
|
|
63
|
+
} else if (m.method === "ui/open-link") { window.__opened.push(m.params.url); reply({}); }
|
|
64
|
+
});
|
|
65
|
+
</script>"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Handler(BaseHTTPRequestHandler):
|
|
69
|
+
def log_message(self, format, *args):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def _send(self, status, body: bytes, content_type: str, extra=None):
|
|
73
|
+
self.send_response(status)
|
|
74
|
+
self.send_header("Content-Type", content_type)
|
|
75
|
+
for key, value in (extra or {}).items():
|
|
76
|
+
self.send_header(key, value)
|
|
77
|
+
self.send_header("Content-Length", str(len(body)))
|
|
78
|
+
self.end_headers()
|
|
79
|
+
self.wfile.write(body)
|
|
80
|
+
|
|
81
|
+
def _proxy(self):
|
|
82
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
83
|
+
body = self.rfile.read(length) if length else None
|
|
84
|
+
headers = {k: v for k, v in self.headers.items() if k.lower() not in ("host", "connection", "content-length")}
|
|
85
|
+
req = urllib.request.Request(BACKEND + self.path[4:], data=body, headers=headers, method=self.command)
|
|
86
|
+
try:
|
|
87
|
+
with urllib.request.urlopen(req, timeout=300) as response:
|
|
88
|
+
status, data, rheaders = response.status, response.read(), response.headers
|
|
89
|
+
except urllib.error.HTTPError as error:
|
|
90
|
+
status, data, rheaders = error.code, error.read(), error.headers
|
|
91
|
+
skip = ("transfer-encoding", "content-length", "connection", "content-encoding")
|
|
92
|
+
self.send_response(status)
|
|
93
|
+
for key, value in rheaders.items():
|
|
94
|
+
if key.lower() not in skip:
|
|
95
|
+
self.send_header(key, value)
|
|
96
|
+
self.send_header("Content-Length", str(len(data)))
|
|
97
|
+
self.end_headers()
|
|
98
|
+
self.wfile.write(data)
|
|
99
|
+
|
|
100
|
+
def do_GET(self):
|
|
101
|
+
if self.path.startswith("/api/"):
|
|
102
|
+
return self._proxy()
|
|
103
|
+
if self.path == "/studio.html":
|
|
104
|
+
html = run(mcp.read_resource(studio.URI))[0].content
|
|
105
|
+
return self._send(200, html.encode(), "text/html", {"Content-Security-Policy": csp()})
|
|
106
|
+
return self._send(200, HOST.encode(), "text/html")
|
|
107
|
+
|
|
108
|
+
def do_OPTIONS(self):
|
|
109
|
+
return self._proxy()
|
|
110
|
+
|
|
111
|
+
def do_PATCH(self):
|
|
112
|
+
return self._proxy()
|
|
113
|
+
|
|
114
|
+
def do_DELETE(self):
|
|
115
|
+
return self._proxy()
|
|
116
|
+
|
|
117
|
+
def do_POST(self):
|
|
118
|
+
if self.path.startswith("/api/"):
|
|
119
|
+
return self._proxy()
|
|
120
|
+
if self.path == "/tool":
|
|
121
|
+
call = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
122
|
+
result = run(mcp.call_tool(call["name"], call.get("arguments") or {}))
|
|
123
|
+
dumped = result.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
124
|
+
return self._send(200, json.dumps(dumped).encode(), "application/json")
|
|
125
|
+
self._send(404, b"", "text/plain")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
print(f"mock host on http://localhost:{PORT}", flush=True)
|
|
130
|
+
ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
|