tavus-cli 0.4.0__tar.gz → 0.4.2__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.
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/pypi-cli.yml +0 -46
- tavus_cli-0.4.2/.release-please-manifest.json +3 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/CHANGELOG.md +14 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/PKG-INFO +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/packages/tavus/pyproject.toml +2 -2
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/packages/tavus-mcp/pyproject.toml +2 -2
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/pyproject.toml +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/__init__.py +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/cli/main.py +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/client/http.py +50 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/tool.py +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/server.py +158 -9
- tavus_cli-0.4.2/tests/integration/test_mcp_server.py +213 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_release_versions.py +0 -30
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/uv.lock +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/package-lock.json +2 -2
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/package.json +1 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/src/tavus.ts +106 -8
- tavus_cli-0.4.2/workers/tavus-mcp/src/version.ts +1 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/test/tavus.test.ts +191 -1
- tavus_cli-0.4.0/.release-please-manifest.json +0 -3
- tavus_cli-0.4.0/tests/integration/test_mcp_server.py +0 -79
- tavus_cli-0.4.0/workers/tavus-mcp/src/version.ts +0 -1
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.env.example +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/ci.yml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/deploy-cloudflare-worker.yml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/pr-reviews.yml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/tail-cloudflare-worker.yml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.github/workflows/validate-staging-deployment.yml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/.gitignore +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/AGENTS.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/README.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/docs/gemini-enterprise/README.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/docs/gemini-enterprise/listing.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/docs/gemini-enterprise/test-queries.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/docs/usage-observability.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/packages/tavus/README.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/packages/tavus-mcp/README.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/release-please-config.json +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/scripts/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/scripts/check_release_versions.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/SKILL.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/agents/openai.yaml +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/references/build-and-verify.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/references/builder-workflow.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/references/chat-mode.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/skills/tavus-agentic-pals/references/examples.md +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/cli/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/auth/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/auth/keyring_store.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/auth/oauth.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/auth/session.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/client/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/daily_transport.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/env.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/errors.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/pal_urls.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/patch.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/build_and_verify.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/capabilities.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/carrier_builder.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/knowledge.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/options.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/quickstart.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/scaffold_embed.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/templates.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/recipes/tool_reuse.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/__init__.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/file_manifest.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/guardrail.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/objective.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/pal.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/src/tavus_mcp/sdk/schemas/pronunciation.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/conftest.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/integration/test_cli.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/integration/test_client.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/integration/test_oauth.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_auth.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_build_and_verify.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_builder.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_capabilities.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_daily_transport.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_env.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_guardrail_schema.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_objective_schema.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_pal_schema.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_patch.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_pronunciation_schema.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_scaffold_embed.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_tool_reuse.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/tests/unit/test_tool_schema.py +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/.dev.vars.example +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/src/index.ts +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/src/oauth-grant.ts +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/src/oauth.ts +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/test/parity.test.ts +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/tsconfig.json +0 -0
- {tavus_cli-0.4.0 → tavus_cli-0.4.2}/workers/tavus-mcp/wrangler.jsonc +0 -0
|
@@ -322,52 +322,6 @@ jobs:
|
|
|
322
322
|
- name: Publish Tavus CLI and its alias packages to PyPI
|
|
323
323
|
run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/ dist/*
|
|
324
324
|
|
|
325
|
-
notify-cvi-automations:
|
|
326
|
-
name: 'Notify #cvi-automations (prod deploy)'
|
|
327
|
-
needs: [preflight, production]
|
|
328
|
-
if: ${{ always() && !cancelled() && needs.preflight.result == 'success' }}
|
|
329
|
-
runs-on: ubuntu-latest
|
|
330
|
-
# A Slack or mention-resolution failure must not change the deploy result.
|
|
331
|
-
continue-on-error: true
|
|
332
|
-
timeout-minutes: 5
|
|
333
|
-
steps:
|
|
334
|
-
- name: Resolve release author mentions
|
|
335
|
-
id: mentions
|
|
336
|
-
continue-on-error: true
|
|
337
|
-
timeout-minutes: 2
|
|
338
|
-
uses: Tavus-Engineering/pr-changelog-action/release-mentions@main
|
|
339
|
-
with:
|
|
340
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
341
|
-
deploy-tag: ${{ needs.preflight.outputs.release_tag }}
|
|
342
|
-
environment: prod
|
|
343
|
-
|
|
344
|
-
- name: 'Post outcome to #cvi-automations'
|
|
345
|
-
env:
|
|
346
|
-
SLACK_BOT_TOKEN: ${{ secrets.CVI_BOT_SLACK_TOKEN }}
|
|
347
|
-
MENTIONS: ${{ steps.mentions.outputs.mentions }}
|
|
348
|
-
DEPLOY_RESULT: ${{ needs.production.result }}
|
|
349
|
-
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
350
|
-
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
351
|
-
run: |
|
|
352
|
-
set -euo pipefail
|
|
353
|
-
AUTHOR_TAGS="${MENTIONS:+${MENTIONS} }"
|
|
354
|
-
CVI_TEAM="<!subteam^S08HUM36HMZ>"
|
|
355
|
-
if [ "$DEPLOY_RESULT" = "success" ]; then
|
|
356
|
-
TEXT=":white_check_mark: ${AUTHOR_TAGS}Tavus MCP <${RUN_URL}|${RELEASE_TAG}> is live in production."
|
|
357
|
-
else
|
|
358
|
-
TEXT=":x: ${CVI_TEAM} Tavus MCP production deploy failed — <${RUN_URL}|run>."
|
|
359
|
-
fi
|
|
360
|
-
|
|
361
|
-
RESPONSE=$(curl --fail-with-body --silent --show-error \
|
|
362
|
-
--connect-timeout 5 --max-time 30 \
|
|
363
|
-
-X POST https://slack.com/api/chat.postMessage \
|
|
364
|
-
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
|
|
365
|
-
-H "Content-Type: application/json; charset=utf-8" \
|
|
366
|
-
--data "$(jq -n --arg channel "C0B9S5Y556V" --arg text "$TEXT" \
|
|
367
|
-
'{channel:$channel, text:$text, unfurl_links:false}')")
|
|
368
|
-
echo "$RESPONSE" | jq .
|
|
369
|
-
echo "$RESPONSE" | jq -e '.ok == true' >/dev/null
|
|
370
|
-
|
|
371
325
|
update-final-board:
|
|
372
326
|
name: Refresh Slack after production
|
|
373
327
|
needs: [preflight, update-pending-board, production]
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.2](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.4.1...v0.4.2) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Reverts
|
|
7
|
+
|
|
8
|
+
* "feat: notify cvi-automations after MCP deploys" ([5aa2112](https://github.com/Tavus-Engineering/tavus-mcp/commit/5aa2112d044e0c956f6e6a1ac4d792c626aa7728))
|
|
9
|
+
|
|
10
|
+
## [0.4.1](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.4.0...v0.4.1) (2026-08-27)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* make MCP list tools compact and complete ([#33](https://github.com/Tavus-Engineering/tavus-mcp/issues/33)) ([9798cad](https://github.com/Tavus-Engineering/tavus-mcp/commit/9798cad82540a188162b1758731f04ae59f082f3))
|
|
16
|
+
|
|
3
17
|
## [0.4.0](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.3.4...v0.4.0) (2026-08-26)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tavus"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2" # x-release-please-version
|
|
4
4
|
description = "Official alias for tavus-cli, the Tavus CLI and MCP server."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"tavus-cli==0.4.
|
|
8
|
+
"tavus-cli==0.4.2", # x-release-please-version
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tavus-mcp"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2" # x-release-please-version
|
|
4
4
|
description = "Official alias for tavus-cli, which provides the tavus-mcp MCP server."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"tavus-cli==0.4.
|
|
8
|
+
"tavus-cli==0.4.2", # x-release-please-version
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
@@ -1462,7 +1462,7 @@ def tool_list(
|
|
|
1462
1462
|
page: int = typer.Option(1),
|
|
1463
1463
|
type: str = typer.Option("user", help="user | system | all"),
|
|
1464
1464
|
name_or_uuid: str | None = typer.Option(None),
|
|
1465
|
-
sort: str = typer.Option("
|
|
1465
|
+
sort: str = typer.Option("asc"),
|
|
1466
1466
|
json_output: bool = typer.Option(False, "--json"),
|
|
1467
1467
|
) -> None:
|
|
1468
1468
|
"""List tools."""
|
|
@@ -27,6 +27,20 @@ BUILDER_LLM_TIMEOUT_S = 120.0
|
|
|
27
27
|
CLIENT_SOURCE_HEADER = "X-Tavus-Client-Source"
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _public_pal_error_body(value: Any) -> Any:
|
|
31
|
+
"""Keep internal persona vocabulary out of public PAL errors."""
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
return value.replace("persona_id", "pal_id")
|
|
34
|
+
if isinstance(value, list):
|
|
35
|
+
return [_public_pal_error_body(item) for item in value]
|
|
36
|
+
if isinstance(value, dict):
|
|
37
|
+
return {
|
|
38
|
+
str(key).replace("persona_id", "pal_id"): _public_pal_error_body(item)
|
|
39
|
+
for key, item in value.items()
|
|
40
|
+
}
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
30
44
|
def _package_version() -> str:
|
|
31
45
|
try:
|
|
32
46
|
return version("tavus-cli")
|
|
@@ -128,6 +142,8 @@ class TavusClient:
|
|
|
128
142
|
body: Any = response.json()
|
|
129
143
|
except ValueError:
|
|
130
144
|
body = response.text
|
|
145
|
+
if path.strip("/").startswith("pals"):
|
|
146
|
+
body = _public_pal_error_body(body)
|
|
131
147
|
raise TavusApiError(
|
|
132
148
|
f"Tavus API {method.upper()} {path} failed with {response.status_code}",
|
|
133
149
|
status_code=response.status_code,
|
|
@@ -149,6 +165,40 @@ class Resource:
|
|
|
149
165
|
async def list(self, **params: Any) -> Any:
|
|
150
166
|
return await self.client.request("GET", f"/{self.name}", params=_clean(params))
|
|
151
167
|
|
|
168
|
+
async def list_all(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
page_size: int = 100,
|
|
172
|
+
start_page: int = 1,
|
|
173
|
+
**params: Any,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Collect a 1-indexed paginated resource without hiding later pages."""
|
|
176
|
+
data: list[Any] = []
|
|
177
|
+
total_count: int | None = None
|
|
178
|
+
for page in range(start_page, start_page + 100):
|
|
179
|
+
response = await self.list(limit=page_size, page=page, **params)
|
|
180
|
+
page_data = (
|
|
181
|
+
response.get("data", [])
|
|
182
|
+
if isinstance(response, dict)
|
|
183
|
+
else response
|
|
184
|
+
if isinstance(response, list)
|
|
185
|
+
else []
|
|
186
|
+
)
|
|
187
|
+
if not isinstance(page_data, list):
|
|
188
|
+
page_data = []
|
|
189
|
+
data.extend(page_data)
|
|
190
|
+
if isinstance(response, dict) and isinstance(response.get("total_count"), int):
|
|
191
|
+
total_count = response["total_count"]
|
|
192
|
+
if not page_data:
|
|
193
|
+
break
|
|
194
|
+
if total_count is not None and len(data) >= total_count:
|
|
195
|
+
break
|
|
196
|
+
if len(page_data) < page_size:
|
|
197
|
+
break
|
|
198
|
+
else:
|
|
199
|
+
raise RuntimeError(f"Tavus API {self.name} pagination exceeded 100 pages")
|
|
200
|
+
return {"data": data, "total_count": total_count or len(data)}
|
|
201
|
+
|
|
152
202
|
async def get(self, resource_id: str, **params: Any) -> Any:
|
|
153
203
|
return await self.client.request(
|
|
154
204
|
"GET",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
from fastmcp import FastMCP
|
|
7
7
|
|
|
@@ -31,7 +31,105 @@ from tavus_mcp.sdk.recipes.scaffold_embed import EmbedTarget, scaffold_embed
|
|
|
31
31
|
from tavus_mcp.sdk.recipes.templates import PalTemplate, pal_from_template
|
|
32
32
|
from tavus_mcp.sdk.recipes.tool_reuse import build_library_match_advisory
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
_READ_ONLY_TOOL_NAMES = {
|
|
35
|
+
"tavus_mcp_health_check",
|
|
36
|
+
"tavus_pal_list",
|
|
37
|
+
"tavus_pal_get",
|
|
38
|
+
"tavus_describe_pal_options",
|
|
39
|
+
"tavus_document_list",
|
|
40
|
+
"tavus_document_get",
|
|
41
|
+
"tavus_document_tags",
|
|
42
|
+
"tavus_document_chunks",
|
|
43
|
+
"tavus_pal_knowledge_list",
|
|
44
|
+
"tavus_skill_list",
|
|
45
|
+
"tavus_skill_get",
|
|
46
|
+
"tavus_pal_skills_list",
|
|
47
|
+
"tavus_pal_capability_catalog",
|
|
48
|
+
"tavus_pal_capabilities_list",
|
|
49
|
+
"tavus_face_list",
|
|
50
|
+
"tavus_scaffold_embed",
|
|
51
|
+
"tavus_resource_list",
|
|
52
|
+
"tavus_voice_list",
|
|
53
|
+
"tavus_guardrail_list",
|
|
54
|
+
"tavus_guardrail_get",
|
|
55
|
+
"tavus_guardrail_tags",
|
|
56
|
+
"tavus_objective_list",
|
|
57
|
+
"tavus_objective_get",
|
|
58
|
+
"tavus_objective_validate",
|
|
59
|
+
"tavus_tool_list",
|
|
60
|
+
"tavus_tool_get",
|
|
61
|
+
"tavus_pronunciation_dictionary_list",
|
|
62
|
+
"tavus_pronunciation_dictionary_get",
|
|
63
|
+
"tavus_pal_tools_list",
|
|
64
|
+
"tavus_builder_list",
|
|
65
|
+
"tavus_builder_get",
|
|
66
|
+
"tavus_builder_chat_history",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_DESTRUCTIVE_TOOL_NAMES = {
|
|
70
|
+
"tavus_pal_delete",
|
|
71
|
+
"tavus_pal_knowledge_remove",
|
|
72
|
+
"tavus_pal_knowledge_replace",
|
|
73
|
+
"tavus_pal_skill_detach",
|
|
74
|
+
"tavus_pal_capability_detach",
|
|
75
|
+
"tavus_conversation_end",
|
|
76
|
+
"tavus_guardrail_delete",
|
|
77
|
+
"tavus_objective_delete",
|
|
78
|
+
"tavus_tool_delete",
|
|
79
|
+
"tavus_pronunciation_dictionary_delete",
|
|
80
|
+
"tavus_pal_tools_detach",
|
|
81
|
+
"tavus_builder_delete",
|
|
82
|
+
"tavus_chat_end",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _annotations_for_tool(name: str) -> dict[str, bool]:
|
|
87
|
+
if name in _READ_ONLY_TOOL_NAMES:
|
|
88
|
+
return {
|
|
89
|
+
"readOnlyHint": True,
|
|
90
|
+
"destructiveHint": False,
|
|
91
|
+
"idempotentHint": True,
|
|
92
|
+
"openWorldHint": True,
|
|
93
|
+
}
|
|
94
|
+
if name in _DESTRUCTIVE_TOOL_NAMES:
|
|
95
|
+
return {
|
|
96
|
+
"readOnlyHint": False,
|
|
97
|
+
"destructiveHint": True,
|
|
98
|
+
"idempotentHint": True,
|
|
99
|
+
"openWorldHint": True,
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
"readOnlyHint": False,
|
|
103
|
+
"destructiveHint": False,
|
|
104
|
+
"idempotentHint": False,
|
|
105
|
+
"openWorldHint": True,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AnnotatedFastMCP(FastMCP):
|
|
110
|
+
"""Apply a conservative annotation policy to every registered Tavus tool."""
|
|
111
|
+
|
|
112
|
+
def tool(self, name_or_fn: Any = None, **kwargs: Any) -> Any:
|
|
113
|
+
if name_or_fn is None and kwargs.get("annotations") is None:
|
|
114
|
+
|
|
115
|
+
def register(fn: Any) -> Any:
|
|
116
|
+
return super(AnnotatedFastMCP, self).tool(
|
|
117
|
+
fn,
|
|
118
|
+
annotations=_annotations_for_tool(kwargs.get("name") or fn.__name__),
|
|
119
|
+
**kwargs,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return register
|
|
123
|
+
if callable(name_or_fn) and kwargs.get("annotations") is None:
|
|
124
|
+
kwargs["annotations"] = _annotations_for_tool(
|
|
125
|
+
kwargs.get("name") or name_or_fn.__name__
|
|
126
|
+
)
|
|
127
|
+
elif isinstance(name_or_fn, str) and kwargs.get("annotations") is None:
|
|
128
|
+
kwargs["annotations"] = _annotations_for_tool(name_or_fn)
|
|
129
|
+
return super().tool(name_or_fn, **kwargs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
mcp = AnnotatedFastMCP(
|
|
35
133
|
"tavus",
|
|
36
134
|
instructions=(
|
|
37
135
|
"PAL tools are first-class objects, not inlined on the PAL. "
|
|
@@ -63,6 +161,47 @@ def _list_data(value: Any) -> list[Any]:
|
|
|
63
161
|
return []
|
|
64
162
|
|
|
65
163
|
|
|
164
|
+
def _summary_value(item: dict[str, Any], *names: str) -> Any:
|
|
165
|
+
for name in names:
|
|
166
|
+
if name in item:
|
|
167
|
+
return item[name]
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _compact_list(value: Any, summarize: Any) -> dict[str, Any]:
|
|
172
|
+
data = [summarize(item) for item in _list_data(value) if isinstance(item, dict)]
|
|
173
|
+
result: dict[str, Any] = {"data": data, "returned_count": len(data)}
|
|
174
|
+
if isinstance(value, dict) and isinstance(value.get("total_count"), int):
|
|
175
|
+
result["total_count"] = value["total_count"]
|
|
176
|
+
else:
|
|
177
|
+
result["total_count"] = len(data)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _pal_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
182
|
+
return {
|
|
183
|
+
"pal_id": _summary_value(item, "pal_id", "persona_id", "uuid"),
|
|
184
|
+
"name": _summary_value(item, "name", "pal_name", "persona_name"),
|
|
185
|
+
"description": _summary_value(
|
|
186
|
+
item,
|
|
187
|
+
"description",
|
|
188
|
+
"pal_description",
|
|
189
|
+
"persona_description",
|
|
190
|
+
),
|
|
191
|
+
"is_published": item.get("is_published"),
|
|
192
|
+
"pipeline_mode": item.get("pipeline_mode"),
|
|
193
|
+
"updated_at": item.get("updated_at"),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _tool_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
198
|
+
return {
|
|
199
|
+
"tool_id": _summary_value(item, "tool_id", "uuid"),
|
|
200
|
+
"name": item.get("name"),
|
|
201
|
+
"is_system_tool": item.get("is_system_tool"),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
66
205
|
def _response_shape(value: Any) -> str:
|
|
67
206
|
if isinstance(value, list):
|
|
68
207
|
return "list"
|
|
@@ -147,8 +286,10 @@ async def tavus_pal_list(
|
|
|
147
286
|
page: int = 0,
|
|
148
287
|
pal_type: str | None = None,
|
|
149
288
|
) -> Any:
|
|
289
|
+
"""List compact PAL summaries. Use ``tavus_pal_get`` for full settings."""
|
|
150
290
|
async with TavusClient.from_env(surface="mcp") as client:
|
|
151
|
-
|
|
291
|
+
response = await client.pals.list(limit=limit, page=page, pal_type=pal_type)
|
|
292
|
+
return _compact_list(response, _pal_summary)
|
|
152
293
|
|
|
153
294
|
|
|
154
295
|
@mcp.tool()
|
|
@@ -974,23 +1115,31 @@ async def tavus_objective_validate(
|
|
|
974
1115
|
|
|
975
1116
|
@mcp.tool()
|
|
976
1117
|
async def tavus_tool_list(
|
|
977
|
-
limit: int =
|
|
1118
|
+
limit: int | None = None,
|
|
978
1119
|
page: int = 1,
|
|
979
1120
|
type: str = "user",
|
|
980
1121
|
name_or_uuid: str | None = None,
|
|
981
|
-
sort:
|
|
1122
|
+
sort: Literal["asc", "desc"] = "asc",
|
|
982
1123
|
) -> Any:
|
|
983
|
-
"""List
|
|
1124
|
+
"""List compact tool summaries.
|
|
1125
|
+
|
|
1126
|
+
Omit ``limit`` to return every matching tool across all API pages. Set
|
|
1127
|
+
``limit`` and ``page`` when one explicit page is enough. ``type`` accepts
|
|
1128
|
+
``"user"``, ``"system"``, or ``"all"``; ``sort`` accepts ``"asc"`` or
|
|
1129
|
+
``"desc"``. Use ``tavus_tool_get`` for the full tool definition.
|
|
1130
|
+
"""
|
|
984
1131
|
params: dict[str, Any] = {
|
|
985
|
-
"limit": limit,
|
|
986
|
-
"page": page,
|
|
987
1132
|
"type": type,
|
|
988
1133
|
"sort": sort,
|
|
989
1134
|
}
|
|
990
1135
|
if name_or_uuid:
|
|
991
1136
|
params["name_or_uuid"] = name_or_uuid
|
|
992
1137
|
async with TavusClient.from_env(surface="mcp") as client:
|
|
993
|
-
|
|
1138
|
+
if limit is None:
|
|
1139
|
+
response = await client.tools.list_all(**params)
|
|
1140
|
+
else:
|
|
1141
|
+
response = await client.tools.list(limit=limit, page=page, **params)
|
|
1142
|
+
return _compact_list(response, _tool_summary)
|
|
994
1143
|
|
|
995
1144
|
|
|
996
1145
|
@mcp.tool()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import respx
|
|
7
|
+
from httpx import Response
|
|
8
|
+
|
|
9
|
+
from tavus_mcp.sdk.errors import TavusApiError
|
|
10
|
+
from tavus_mcp.server import (
|
|
11
|
+
mcp,
|
|
12
|
+
tavus_pal_create,
|
|
13
|
+
tavus_pal_get,
|
|
14
|
+
tavus_pal_list,
|
|
15
|
+
tavus_tool_list,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_mcp_server_imports_and_registers_tools() -> None:
|
|
21
|
+
tools = {tool.name: tool for tool in await mcp.list_tools()}
|
|
22
|
+
|
|
23
|
+
assert "tavus_mcp_health_check" in tools
|
|
24
|
+
assert "tavus_pal_list" in tools
|
|
25
|
+
assert "tavus_describe_pal_options" in tools
|
|
26
|
+
assert "tavus_skill_list" in tools
|
|
27
|
+
assert "tavus_document_list" in tools
|
|
28
|
+
assert "tavus_document_upload" in tools
|
|
29
|
+
assert "tavus_document_chunks" in tools
|
|
30
|
+
assert "tavus_pal_knowledge_list" in tools
|
|
31
|
+
assert "tavus_pal_knowledge_add" in tools
|
|
32
|
+
assert "tavus_pal_knowledge_remove" in tools
|
|
33
|
+
assert "tavus_pal_skills_list" in tools
|
|
34
|
+
assert "tavus_pal_capabilities_list" in tools
|
|
35
|
+
assert "tavus_pal_capability_attach" in tools
|
|
36
|
+
assert "tavus_pal_capability_detach" in tools
|
|
37
|
+
assert "tavus_pal_from_template" in tools
|
|
38
|
+
assert "tavus_scaffold_embed" in tools
|
|
39
|
+
build_schema = tools["tavus_pal_build_and_verify"].parameters
|
|
40
|
+
assert "probes" in build_schema["properties"]
|
|
41
|
+
assert tools["tavus_tool_list"].parameters["properties"]["sort"]["enum"] == [
|
|
42
|
+
"asc",
|
|
43
|
+
"desc",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
assert all(tool.annotations is not None for tool in tools.values())
|
|
47
|
+
health_annotations = tools["tavus_mcp_health_check"].annotations
|
|
48
|
+
assert health_annotations.readOnlyHint is True
|
|
49
|
+
assert health_annotations.destructiveHint is False
|
|
50
|
+
assert health_annotations.idempotentHint is True
|
|
51
|
+
assert health_annotations.openWorldHint is True
|
|
52
|
+
assert tools["tavus_pal_create"].annotations.readOnlyHint is False
|
|
53
|
+
assert tools["tavus_pal_create"].annotations.destructiveHint is False
|
|
54
|
+
assert tools["tavus_pal_delete"].annotations.destructiveHint is True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
@respx.mock
|
|
59
|
+
async def test_mcp_requests_identify_the_mcp_surface(clean_env: Path) -> None:
|
|
60
|
+
(clean_env / ".env").write_text(
|
|
61
|
+
"TAVUS_ENV=TEST\n"
|
|
62
|
+
"TAVUS_API_KEY=test-key\n"
|
|
63
|
+
"TAVUS_PUBLIC_API_BASE_URL=https://api.example.test/v2\n"
|
|
64
|
+
)
|
|
65
|
+
route = respx.get("https://api.example.test/v2/pals").mock(
|
|
66
|
+
return_value=Response(
|
|
67
|
+
200,
|
|
68
|
+
json={
|
|
69
|
+
"data": [
|
|
70
|
+
{
|
|
71
|
+
"pal_id": "p-summary",
|
|
72
|
+
"pal_name": "Summary PAL",
|
|
73
|
+
"pal_description": "A compact example.",
|
|
74
|
+
"is_published": True,
|
|
75
|
+
"pipeline_mode": "full",
|
|
76
|
+
"updated_at": "2026-08-25T12:00:00Z",
|
|
77
|
+
"system_prompt": "This must not appear in a list response.",
|
|
78
|
+
"context": "Nor should this.",
|
|
79
|
+
"layers": {"llm": {"model": "private-detail"}},
|
|
80
|
+
"owner_id": 123,
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"total_count": 312,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert await tavus_pal_list() == {
|
|
89
|
+
"data": [
|
|
90
|
+
{
|
|
91
|
+
"pal_id": "p-summary",
|
|
92
|
+
"name": "Summary PAL",
|
|
93
|
+
"description": "A compact example.",
|
|
94
|
+
"is_published": True,
|
|
95
|
+
"pipeline_mode": "full",
|
|
96
|
+
"updated_at": "2026-08-25T12:00:00Z",
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"returned_count": 1,
|
|
100
|
+
"total_count": 312,
|
|
101
|
+
}
|
|
102
|
+
request = route.calls.last.request
|
|
103
|
+
assert request.headers["user-agent"].startswith("tavus-mcp/")
|
|
104
|
+
assert request.headers["x-tavus-client-source"] == "mcp"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
@respx.mock
|
|
109
|
+
async def test_tool_list_collects_every_page_and_returns_summaries(clean_env: Path) -> None:
|
|
110
|
+
(clean_env / ".env").write_text(
|
|
111
|
+
"TAVUS_ENV=TEST\n"
|
|
112
|
+
"TAVUS_API_KEY=test-key\n"
|
|
113
|
+
"TAVUS_PUBLIC_API_BASE_URL=https://api.example.test/v2\n"
|
|
114
|
+
)
|
|
115
|
+
first_page = [
|
|
116
|
+
{
|
|
117
|
+
"tool_id": f"t-{index}",
|
|
118
|
+
"name": f"tool_{index}",
|
|
119
|
+
"description": f"Tool {index}",
|
|
120
|
+
"origin": "llm",
|
|
121
|
+
"trigger_type": "in_call",
|
|
122
|
+
"is_system_tool": False,
|
|
123
|
+
"updated_at": "2026-08-25T12:00:00Z",
|
|
124
|
+
"parameters": {"large": "detail"},
|
|
125
|
+
"owner_id": 123,
|
|
126
|
+
}
|
|
127
|
+
for index in range(100)
|
|
128
|
+
]
|
|
129
|
+
route = respx.get("https://api.example.test/v2/tools").mock(
|
|
130
|
+
side_effect=[
|
|
131
|
+
Response(200, json={"data": first_page, "total_count": 101}),
|
|
132
|
+
Response(
|
|
133
|
+
200,
|
|
134
|
+
json={
|
|
135
|
+
"data": [
|
|
136
|
+
{
|
|
137
|
+
"tool_id": "t-100",
|
|
138
|
+
"name": "tool_100",
|
|
139
|
+
"description": "Tool 100",
|
|
140
|
+
"origin": "llm",
|
|
141
|
+
"trigger_type": "in_call",
|
|
142
|
+
"is_system_tool": False,
|
|
143
|
+
"updated_at": "2026-08-25T12:00:00Z",
|
|
144
|
+
"parameters": {"large": "detail"},
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"total_count": 101,
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
result = await tavus_tool_list(sort="desc")
|
|
154
|
+
|
|
155
|
+
assert result["returned_count"] == 101
|
|
156
|
+
assert result["total_count"] == 101
|
|
157
|
+
assert result["data"][-1] == {
|
|
158
|
+
"tool_id": "t-100",
|
|
159
|
+
"name": "tool_100",
|
|
160
|
+
"is_system_tool": False,
|
|
161
|
+
}
|
|
162
|
+
assert len(route.calls) == 2
|
|
163
|
+
assert route.calls[0].request.url.params["sort"] == "desc"
|
|
164
|
+
assert route.calls[0].request.url.params["page"] == "1"
|
|
165
|
+
assert route.calls[1].request.url.params["page"] == "2"
|
|
166
|
+
assert "parameters" not in result["data"][-1]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
@respx.mock
|
|
171
|
+
async def test_pal_get_uses_public_vocabulary_in_errors(clean_env: Path) -> None:
|
|
172
|
+
(clean_env / ".env").write_text(
|
|
173
|
+
"TAVUS_ENV=TEST\n"
|
|
174
|
+
"TAVUS_API_KEY=test-key\n"
|
|
175
|
+
"TAVUS_PUBLIC_API_BASE_URL=https://api.example.test/v2\n"
|
|
176
|
+
)
|
|
177
|
+
respx.get("https://api.example.test/v2/pals/p-missing").mock(
|
|
178
|
+
return_value=Response(400, json={"message": "Invalid persona_id"})
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with pytest.raises(TavusApiError) as error:
|
|
182
|
+
await tavus_pal_get("p-missing")
|
|
183
|
+
|
|
184
|
+
assert error.value.status_code == 400
|
|
185
|
+
assert error.value.body == {"message": "Invalid pal_id"}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
@respx.mock
|
|
190
|
+
async def test_pal_create_returns_matching_pal_maker_url(clean_env: Path) -> None:
|
|
191
|
+
(clean_env / ".env").write_text(
|
|
192
|
+
"TAVUS_ENV=PROD\n"
|
|
193
|
+
"TAVUS_API_KEY=test-key\n"
|
|
194
|
+
"TAVUS_PUBLIC_API_BASE_URL=https://api.example.test/v2\n"
|
|
195
|
+
"TAVUS_PAL_MAKER_URL=https://maker.example.test\n"
|
|
196
|
+
)
|
|
197
|
+
respx.post("https://api.example.test/v2/pals").mock(
|
|
198
|
+
return_value=Response(
|
|
199
|
+
200,
|
|
200
|
+
json={"pal_id": "p-created", "pal_name": "Builder Link Test"},
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
created = await tavus_pal_create(
|
|
205
|
+
system_prompt="You are a test PAL.",
|
|
206
|
+
pal_name="Builder Link Test",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert created == {
|
|
210
|
+
"pal_id": "p-created",
|
|
211
|
+
"pal_name": "Builder Link Test",
|
|
212
|
+
"pal_url": "https://maker.example.test/dev/pals/update?pal_id=p-created",
|
|
213
|
+
}
|