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.
Files changed (52) hide show
  1. liveface_mcp-0.1.0/.gitignore +4 -0
  2. liveface_mcp-0.1.0/LICENSE +30 -0
  3. liveface_mcp-0.1.0/PKG-INFO +135 -0
  4. liveface_mcp-0.1.0/PYPI.md +89 -0
  5. liveface_mcp-0.1.0/README.md +114 -0
  6. liveface_mcp-0.1.0/build.sh +16 -0
  7. liveface_mcp-0.1.0/e2e/README.md +29 -0
  8. liveface_mcp-0.1.0/e2e/harness.py +130 -0
  9. liveface_mcp-0.1.0/e2e/studio.e2e.mjs +130 -0
  10. liveface_mcp-0.1.0/liveface_mcp/__init__.py +0 -0
  11. liveface_mcp-0.1.0/liveface_mcp/account.py +236 -0
  12. liveface_mcp-0.1.0/liveface_mcp/client.py +146 -0
  13. liveface_mcp-0.1.0/liveface_mcp/config.py +74 -0
  14. liveface_mcp-0.1.0/liveface_mcp/creation.py +240 -0
  15. liveface_mcp-0.1.0/liveface_mcp/server.py +41 -0
  16. liveface_mcp-0.1.0/liveface_mcp/services.py +45 -0
  17. liveface_mcp-0.1.0/liveface_mcp/tools/__init__.py +16 -0
  18. liveface_mcp-0.1.0/liveface_mcp/tools/account.py +50 -0
  19. liveface_mcp-0.1.0/liveface_mcp/tools/speak.py +68 -0
  20. liveface_mcp-0.1.0/liveface_mcp/tools/studio.py +359 -0
  21. liveface_mcp-0.1.0/liveface_mcp/transport.py +98 -0
  22. liveface_mcp-0.1.0/liveface_mcp/web/studio/app.ts +51 -0
  23. liveface_mcp-0.1.0/liveface_mcp/web/studio/assets.d.ts +4 -0
  24. liveface_mcp-0.1.0/liveface_mcp/web/studio/bridge.ts +153 -0
  25. liveface_mcp-0.1.0/liveface_mcp/web/studio/dom.ts +26 -0
  26. liveface_mcp-0.1.0/liveface_mcp/web/studio/main.ts +37 -0
  27. liveface_mcp-0.1.0/liveface_mcp/web/studio/photo.ts +27 -0
  28. liveface_mcp-0.1.0/liveface_mcp/web/studio/pictures.ts +47 -0
  29. liveface_mcp-0.1.0/liveface_mcp/web/studio/recorder.ts +105 -0
  30. liveface_mcp-0.1.0/liveface_mcp/web/studio/stage.ts +53 -0
  31. liveface_mcp-0.1.0/liveface_mcp/web/studio/types.ts +88 -0
  32. liveface_mcp-0.1.0/liveface_mcp/web/studio/ui.ts +51 -0
  33. liveface_mcp-0.1.0/liveface_mcp/web/studio/views/connect.ts +69 -0
  34. liveface_mcp-0.1.0/liveface_mcp/web/studio/views/create.ts +340 -0
  35. liveface_mcp-0.1.0/liveface_mcp/web/studio/views/library.ts +251 -0
  36. liveface_mcp-0.1.0/liveface_mcp/web/studio/wording.ts +40 -0
  37. liveface_mcp-0.1.0/liveface_mcp/web/studio.html +58 -0
  38. liveface_mcp-0.1.0/liveface_mcp/web/studio.js +21 -0
  39. liveface_mcp-0.1.0/liveface_mcp/web/window.entry.ts +165 -0
  40. liveface_mcp-0.1.0/liveface_mcp/web/window.html +19 -0
  41. liveface_mcp-0.1.0/liveface_mcp/web/window.js +21 -0
  42. liveface_mcp-0.1.0/liveface_mcp/window.py +194 -0
  43. liveface_mcp-0.1.0/pyproject.toml +49 -0
  44. liveface_mcp-0.1.0/tests/__init__.py +0 -0
  45. liveface_mcp-0.1.0/tests/fakes.py +46 -0
  46. liveface_mcp-0.1.0/tests/test_account.py +141 -0
  47. liveface_mcp-0.1.0/tests/test_config.py +44 -0
  48. liveface_mcp-0.1.0/tests/test_creation.py +230 -0
  49. liveface_mcp-0.1.0/tests/test_studio.py +238 -0
  50. liveface_mcp-0.1.0/tests/test_transport.py +83 -0
  51. liveface_mcp-0.1.0/tests/test_window.py +59 -0
  52. liveface_mcp-0.1.0/tsconfig.json +16 -0
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ liveface_mcp/web/*.js
3
+ *.egg-info/
4
+ e2e/out/
@@ -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()