tavus-cli 0.4.4__tar.gz → 0.5.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.
- tavus_cli-0.5.0/.release-please-manifest.json +3 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/CHANGELOG.md +14 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/PKG-INFO +1 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/usage-observability.md +29 -7
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus/pyproject.toml +2 -2
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus-mcp/pyproject.toml +2 -2
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/pyproject.toml +1 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/__init__.py +1 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/client/http.py +41 -2
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/server.py +48 -20
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_mcp_server.py +35 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/uv.lock +1 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/package-lock.json +2 -2
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/package.json +1 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/oauth.ts +14 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/tavus.ts +74 -1
- tavus_cli-0.5.0/workers/tavus-mcp/src/version.ts +1 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/test/tavus.test.ts +87 -0
- tavus_cli-0.4.4/.release-please-manifest.json +0 -3
- tavus_cli-0.4.4/workers/tavus-mcp/src/version.ts +0 -1
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.env.example +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/ci.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/deploy-cloudflare-worker.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/pr-reviews.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/pypi-cli.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/tail-cloudflare-worker.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/validate-staging-deployment.yml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.gitignore +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/AGENTS.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/README.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/README.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/listing.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/test-queries.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus/README.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus-mcp/README.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/release-please-config.json +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/scripts/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/scripts/check_release_versions.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/SKILL.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/agents/openai.yaml +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/build-and-verify.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/builder-workflow.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/chat-mode.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/examples.md +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/cli/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/cli/main.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/keyring_store.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/oauth.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/session.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/client/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/daily_transport.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/env.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/errors.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/pal_urls.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/patch.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/build_and_verify.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/capabilities.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/carrier_builder.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/knowledge.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/options.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/quickstart.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/scaffold_embed.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/templates.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/tool_reuse.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/__init__.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/file_manifest.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/guardrail.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/objective.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/pal.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/pronunciation.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/tool.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/conftest.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_cli.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_client.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_oauth.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_auth.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_build_and_verify.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_builder.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_capabilities.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_daily_transport.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_env.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_guardrail_schema.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_objective_schema.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_pal_schema.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_patch.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_pronunciation_schema.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_release_versions.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_scaffold_embed.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_tool_reuse.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_tool_schema.py +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/.dev.vars.example +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/index.ts +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/oauth-grant.ts +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/test/parity.test.ts +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/tsconfig.json +0 -0
- {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/wrangler.jsonc +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.4.5...v0.5.0) (2026-09-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* record why agents call each MCP tool ([#51](https://github.com/Tavus-Engineering/tavus-mcp/issues/51)) ([1059388](https://github.com/Tavus-Engineering/tavus-mcp/commit/10593882a1f08a87a29e14f24bfdc7a3f53f0797))
|
|
9
|
+
|
|
10
|
+
## [0.4.5](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.4.4...v0.4.5) (2026-09-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* accept Cursor and IPv6 loopback DCR callbacks ([#49](https://github.com/Tavus-Engineering/tavus-mcp/issues/49)) ([8c9d9f3](https://github.com/Tavus-Engineering/tavus-mcp/commit/8c9d9f3a0cd42bb51080a84b0982a9c4ad64f175))
|
|
16
|
+
|
|
3
17
|
## [0.4.4](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.4.3...v0.4.4) (2026-09-10)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -9,9 +9,27 @@ Tavus API requests with a source-specific user agent:
|
|
|
9
9
|
| Local MCP server | `tavus-mcp/<version>` |
|
|
10
10
|
| Hosted MCP Worker | `tavus-mcp-worker/<version>` |
|
|
11
11
|
|
|
12
|
-
They also send the matching low-cardinality `X-Tavus-Client-Source` header.
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
They also send the matching low-cardinality `X-Tavus-Client-Source` header.
|
|
13
|
+
|
|
14
|
+
## Tool intent
|
|
15
|
+
|
|
16
|
+
Every MCP tool on both servers accepts an optional `intent` argument. The
|
|
17
|
+
calling agent uses it to say, in one sentence, why it called the tool and what
|
|
18
|
+
the user is trying to get done. Agents may leave it out, and a call never fails
|
|
19
|
+
because of it.
|
|
20
|
+
|
|
21
|
+
Each Tavus API request made during a tool call carries two more headers:
|
|
22
|
+
|
|
23
|
+
| Header | Value |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `X-Tavus-Client-Tool` | The MCP tool name, such as `tavus_pal_get` |
|
|
26
|
+
| `X-Tavus-Client-Intent` | The intent, when given: whitespace collapsed, cut to 500 characters, and percent-encoded outside printable ASCII (`%` becomes `%25`) |
|
|
27
|
+
|
|
28
|
+
Request-handler logs all three `X-Tavus-Client-*` headers in the `headers` field
|
|
29
|
+
of `log_request_data`, so intent sits next to the URL and joins to `user_id` on
|
|
30
|
+
`trace_id` like the rest of this data. Decode the intent with any URL decoder,
|
|
31
|
+
such as Python's `urllib.parse.unquote`. One tool call can make several API
|
|
32
|
+
requests, and each one carries the same tool name and intent.
|
|
15
33
|
|
|
16
34
|
## SigNoz data path
|
|
17
35
|
|
|
@@ -59,7 +77,11 @@ historical rows until this source tagging is released.
|
|
|
59
77
|
- Counts are downstream Tavus API requests, not top-level MCP tool calls or CLI
|
|
60
78
|
invocations. Local-only operations such as help, auth status, path listing, and
|
|
61
79
|
embed scaffolding do not generate an API request and are not counted.
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
- The source tags contain only the product surface and package version.
|
|
81
|
+
Request-handler continues applying its existing API-key truncation and 64 KiB
|
|
82
|
+
body-redaction policy.
|
|
83
|
+
- Intent is free text written by the calling agent. The argument description
|
|
84
|
+
asks agents to leave out secrets and personal details, but nothing enforces
|
|
85
|
+
that, so treat logged intent with the same care as logged request bodies.
|
|
86
|
+
- Tool calls that make no Tavus API request, such as `tavus_scaffold_embed`, do
|
|
87
|
+
not record their intent.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tavus"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0" # 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.
|
|
8
|
+
"tavus-cli==0.5.0", # 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.
|
|
3
|
+
version = "0.5.0" # 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.
|
|
8
|
+
"tavus-cli==0.5.0", # x-release-please-version
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
@@ -2,9 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json as _json
|
|
4
4
|
import mimetypes
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
5
8
|
from importlib.metadata import PackageNotFoundError, version
|
|
6
9
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Self
|
|
10
|
+
from typing import Any, NamedTuple, Self
|
|
11
|
+
from urllib.parse import quote
|
|
8
12
|
|
|
9
13
|
import httpx
|
|
10
14
|
|
|
@@ -25,6 +29,33 @@ from tavus_mcp.sdk.schemas.tool import AttachToolsBody, ToolCreate, ToolUpdate
|
|
|
25
29
|
|
|
26
30
|
BUILDER_LLM_TIMEOUT_S = 120.0
|
|
27
31
|
CLIENT_SOURCE_HEADER = "X-Tavus-Client-Source"
|
|
32
|
+
CLIENT_TOOL_HEADER = "X-Tavus-Client-Tool"
|
|
33
|
+
CLIENT_INTENT_HEADER = "X-Tavus-Client-Intent"
|
|
34
|
+
MAX_INTENT_LENGTH = 500
|
|
35
|
+
# Header values must be printable ASCII. Everything else, and "%" itself, is
|
|
36
|
+
# percent-encoded so request-handler logs stay readable and decodable.
|
|
37
|
+
_INTENT_HEADER_SAFE = "".join(chr(code) for code in range(0x20, 0x7F) if chr(code) != "%")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _ToolCall(NamedTuple):
|
|
41
|
+
tool: str
|
|
42
|
+
intent: str | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_current_tool_call: ContextVar[_ToolCall | None] = ContextVar(
|
|
46
|
+
"tavus_current_tool_call", default=None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@contextmanager
|
|
51
|
+
def tool_call_context(tool: str, intent: str | None) -> Iterator[None]:
|
|
52
|
+
"""Tag Tavus API requests made inside an MCP tool call with its name and intent."""
|
|
53
|
+
normalized = " ".join((intent or "").split())[:MAX_INTENT_LENGTH] or None
|
|
54
|
+
token = _current_tool_call.set(_ToolCall(tool, normalized))
|
|
55
|
+
try:
|
|
56
|
+
yield
|
|
57
|
+
finally:
|
|
58
|
+
_current_tool_call.reset(token)
|
|
28
59
|
|
|
29
60
|
|
|
30
61
|
def _public_pal_error_body(value: Any) -> Any:
|
|
@@ -59,11 +90,19 @@ def _client_headers(api_key: str, surface: str) -> dict[str, str]:
|
|
|
59
90
|
raise ValueError(
|
|
60
91
|
"surface must contain only lowercase letters, numbers, '.', '_', or '-'"
|
|
61
92
|
)
|
|
62
|
-
|
|
93
|
+
headers = {
|
|
63
94
|
"x-api-key": api_key,
|
|
64
95
|
"User-Agent": f"tavus-{normalized_surface}/{_package_version()}",
|
|
65
96
|
CLIENT_SOURCE_HEADER: normalized_surface,
|
|
66
97
|
}
|
|
98
|
+
tool_call = _current_tool_call.get()
|
|
99
|
+
if tool_call is not None:
|
|
100
|
+
headers[CLIENT_TOOL_HEADER] = tool_call.tool
|
|
101
|
+
if tool_call.intent:
|
|
102
|
+
headers[CLIENT_INTENT_HEADER] = quote(
|
|
103
|
+
tool_call.intent, safe=_INTENT_HEADER_SAFE, errors="replace"
|
|
104
|
+
)
|
|
105
|
+
return headers
|
|
67
106
|
|
|
68
107
|
|
|
69
108
|
class TavusClient:
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
3
5
|
import time
|
|
4
|
-
from typing import Any, Literal
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
5
7
|
|
|
6
8
|
from fastmcp import FastMCP
|
|
9
|
+
from pydantic import Field
|
|
7
10
|
|
|
8
11
|
from tavus_mcp.sdk import daily_transport
|
|
9
12
|
from tavus_mcp.sdk.auth.session import get_session
|
|
10
|
-
from tavus_mcp.sdk.client.http import TavusClient
|
|
13
|
+
from tavus_mcp.sdk.client.http import TavusClient, tool_call_context
|
|
11
14
|
from tavus_mcp.sdk.env import load_config
|
|
12
15
|
from tavus_mcp.sdk.recipes.build_and_verify import build_and_verify
|
|
13
16
|
from tavus_mcp.sdk.recipes.capabilities import (
|
|
@@ -106,27 +109,52 @@ def _annotations_for_tool(name: str) -> dict[str, bool]:
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
|
|
112
|
+
INTENT_DESCRIPTION = (
|
|
113
|
+
"Optional. One sentence on why you are calling this tool: what the user is "
|
|
114
|
+
"trying to get done. Tavus stores it to improve these tools. Leave out "
|
|
115
|
+
"secrets and personal details."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
_INTENT_PARAMETER = inspect.Parameter(
|
|
119
|
+
"intent",
|
|
120
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
121
|
+
default=None,
|
|
122
|
+
annotation=Annotated[str | None, Field(description=INTENT_DESCRIPTION)],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _with_intent(fn: Any, tool_name: str) -> Any:
|
|
127
|
+
"""Add the optional ``intent`` argument and tag the call's API requests with it."""
|
|
128
|
+
|
|
129
|
+
@functools.wraps(fn)
|
|
130
|
+
async def call_with_intent(*args: Any, intent: str | None = None, **kwargs: Any) -> Any:
|
|
131
|
+
with tool_call_context(tool_name, intent):
|
|
132
|
+
return await fn(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
signature = inspect.signature(fn)
|
|
135
|
+
call_with_intent.__signature__ = signature.replace( # type: ignore[attr-defined]
|
|
136
|
+
parameters=[*signature.parameters.values(), _INTENT_PARAMETER]
|
|
137
|
+
)
|
|
138
|
+
call_with_intent.__annotations__ = {
|
|
139
|
+
**fn.__annotations__,
|
|
140
|
+
"intent": _INTENT_PARAMETER.annotation,
|
|
141
|
+
}
|
|
142
|
+
return call_with_intent
|
|
143
|
+
|
|
144
|
+
|
|
109
145
|
class AnnotatedFastMCP(FastMCP):
|
|
110
|
-
"""Apply
|
|
146
|
+
"""Apply the Tavus annotation and intent policy to every registered tool."""
|
|
111
147
|
|
|
112
148
|
def tool(self, name_or_fn: Any = None, **kwargs: Any) -> Any:
|
|
113
|
-
if name_or_fn
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
149
|
+
if isinstance(name_or_fn, str):
|
|
150
|
+
kwargs["name"] = name_or_fn
|
|
151
|
+
name_or_fn = None
|
|
152
|
+
if name_or_fn is None:
|
|
153
|
+
return lambda fn: self.tool(fn, **kwargs)
|
|
154
|
+
name = kwargs.get("name") or name_or_fn.__name__
|
|
155
|
+
if kwargs.get("annotations") is None:
|
|
156
|
+
kwargs["annotations"] = _annotations_for_tool(name)
|
|
157
|
+
return super().tool(_with_intent(name_or_fn, name), **kwargs)
|
|
130
158
|
|
|
131
159
|
|
|
132
160
|
mcp = AnnotatedFastMCP(
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from urllib.parse import unquote
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
import respx
|
|
8
|
+
from fastmcp import Client
|
|
7
9
|
from httpx import Response
|
|
8
10
|
|
|
9
11
|
from tavus_mcp.sdk.errors import TavusApiError
|
|
@@ -102,6 +104,39 @@ async def test_mcp_requests_identify_the_mcp_surface(clean_env: Path) -> None:
|
|
|
102
104
|
request = route.calls.last.request
|
|
103
105
|
assert request.headers["user-agent"].startswith("tavus-mcp/")
|
|
104
106
|
assert request.headers["x-tavus-client-source"] == "mcp"
|
|
107
|
+
assert request.headers["x-tavus-client-tool"] == "tavus_pal_list"
|
|
108
|
+
assert "x-tavus-client-intent" not in request.headers
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_every_tool_offers_an_optional_intent() -> None:
|
|
113
|
+
for tool in await mcp.list_tools():
|
|
114
|
+
assert "intent" in tool.parameters["properties"], tool.name
|
|
115
|
+
assert "intent" not in tool.parameters.get("required", []), tool.name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
@respx.mock
|
|
120
|
+
async def test_mcp_requests_carry_the_encoded_intent(clean_env: Path) -> None:
|
|
121
|
+
(clean_env / ".env").write_text(
|
|
122
|
+
"TAVUS_ENV=TEST\n"
|
|
123
|
+
"TAVUS_API_KEY=test-key\n"
|
|
124
|
+
"TAVUS_PUBLIC_API_BASE_URL=https://api.example.test/v2\n"
|
|
125
|
+
)
|
|
126
|
+
route = respx.get("https://api.example.test/v2/pals/p1").mock(
|
|
127
|
+
return_value=Response(200, json={"pal_id": "p1"})
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async with Client(mcp) as client:
|
|
131
|
+
await client.call_tool(
|
|
132
|
+
"tavus_pal_get",
|
|
133
|
+
{"pal_id": "p1", "intent": " Check the café PAL\nbefore 100% launch "},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
headers = route.calls.last.request.headers
|
|
137
|
+
assert headers["x-tavus-client-tool"] == "tavus_pal_get"
|
|
138
|
+
assert headers["x-tavus-client-intent"] == "Check the caf%C3%A9 PAL before 100%25 launch"
|
|
139
|
+
assert unquote(headers["x-tavus-client-intent"]) == "Check the café PAL before 100% launch"
|
|
105
140
|
|
|
106
141
|
|
|
107
142
|
@pytest.mark.asyncio
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tavus/tavus-mcp-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@tavus/tavus-mcp-worker",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
12
12
|
"agents": "^0.12.2",
|
|
@@ -712,7 +712,10 @@ function isAllowedResource(rawResource: string, expectedResource: string): boole
|
|
|
712
712
|
function isLoopbackUrl(raw: string): boolean {
|
|
713
713
|
try {
|
|
714
714
|
const url = new URL(raw);
|
|
715
|
-
return
|
|
715
|
+
return (
|
|
716
|
+
url.protocol === "http:" &&
|
|
717
|
+
(url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]")
|
|
718
|
+
);
|
|
716
719
|
} catch {
|
|
717
720
|
return false;
|
|
718
721
|
}
|
|
@@ -764,6 +767,16 @@ function trustedRemoteClientName(raw: string): string | undefined {
|
|
|
764
767
|
) {
|
|
765
768
|
return "ChatGPT";
|
|
766
769
|
}
|
|
770
|
+
// Cursor registers its hosted callback rather than a loopback listener when
|
|
771
|
+
// its loopback-redirect flag is off, so DCR fails without this entry.
|
|
772
|
+
if (
|
|
773
|
+
url.protocol === "https:" &&
|
|
774
|
+
(url.hostname === "cursor.com" || url.hostname === "www.cursor.com") &&
|
|
775
|
+
url.pathname === "/agents/mcp/oauth/callback" &&
|
|
776
|
+
!url.search
|
|
777
|
+
) {
|
|
778
|
+
return "Cursor";
|
|
779
|
+
}
|
|
767
780
|
return undefined;
|
|
768
781
|
}
|
|
769
782
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
1
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
2
6
|
import { z } from "zod";
|
|
3
7
|
|
|
4
8
|
import type { OAuthGrantKind } from "./oauth-grant";
|
|
@@ -83,6 +87,66 @@ export const DEFAULT_PAL_LLM_MODEL = "tavus-gemma-4";
|
|
|
83
87
|
const CLIENT_SOURCE = "mcp-worker";
|
|
84
88
|
const CLIENT_VERSION = VERSION;
|
|
85
89
|
|
|
90
|
+
export const INTENT_DESCRIPTION =
|
|
91
|
+
"Optional. One sentence on why you are calling this tool: what the user is " +
|
|
92
|
+
"trying to get done. Tavus stores it to improve these tools. Leave out " +
|
|
93
|
+
"secrets and personal details.";
|
|
94
|
+
const MAX_INTENT_LENGTH = 500;
|
|
95
|
+
|
|
96
|
+
interface ToolCall {
|
|
97
|
+
tool: string;
|
|
98
|
+
intent?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type ToolHandler = (args: Record<string, unknown>, extra: unknown) => unknown;
|
|
102
|
+
|
|
103
|
+
const currentToolCall = new AsyncLocalStorage<ToolCall>();
|
|
104
|
+
|
|
105
|
+
function normalizeIntent(intent: unknown): string | undefined {
|
|
106
|
+
if (typeof intent !== "string") return undefined;
|
|
107
|
+
const collapsed = intent.trim().split(/\s+/).join(" ");
|
|
108
|
+
// Truncate by code point so an emoji is never split into a lone surrogate.
|
|
109
|
+
return Array.from(collapsed).slice(0, MAX_INTENT_LENGTH).join("") || undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Header values must be printable ASCII. Everything else, and "%" itself, is
|
|
113
|
+
// percent-encoded so request-handler logs stay readable and decodable.
|
|
114
|
+
function intentHeaderValue(intent: string): string {
|
|
115
|
+
return intent.replace(/[^\x20-\x24\x26-\x7e]/gu, (char) =>
|
|
116
|
+
/\p{Cs}/u.test(char) ? "?" : encodeURIComponent(char),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Adds the optional intent argument to every tool and tags the Tavus API
|
|
121
|
+
// requests a call makes with the tool name and intent.
|
|
122
|
+
function addToolIntent(server: McpServer): void {
|
|
123
|
+
const register = server.tool.bind(server) as (...args: unknown[]) => RegisteredTool;
|
|
124
|
+
server.tool = ((...args: unknown[]) => {
|
|
125
|
+
if (args.length !== 5) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Register ${String(args[0])} with server.tool(name, description, schema, annotations, handler).`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const [name, description, schema, annotations, handler] = args as [
|
|
131
|
+
string,
|
|
132
|
+
string,
|
|
133
|
+
z.ZodRawShape,
|
|
134
|
+
ToolAnnotations,
|
|
135
|
+
ToolHandler,
|
|
136
|
+
];
|
|
137
|
+
return register(
|
|
138
|
+
name,
|
|
139
|
+
description,
|
|
140
|
+
{ ...schema, intent: z.string().optional().describe(INTENT_DESCRIPTION) },
|
|
141
|
+
annotations,
|
|
142
|
+
({ intent, ...toolArgs }: Record<string, unknown>, extra: unknown) =>
|
|
143
|
+
currentToolCall.run({ tool: name, intent: normalizeIntent(intent) }, () =>
|
|
144
|
+
handler(toolArgs, extra),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
}) as McpServer["tool"];
|
|
148
|
+
}
|
|
149
|
+
|
|
86
150
|
const DEFAULT_PUBLIC_API_BASE_URLS: Record<TavusEnvironment, string> = {
|
|
87
151
|
TEST: "https://test.rqh.tavusapi.com/v2",
|
|
88
152
|
STG: "https://stg.rqh.tavusapi.com/v2",
|
|
@@ -449,6 +513,7 @@ export function createTavusMcpServer(env: Env, apiKey?: string): McpServer {
|
|
|
449
513
|
name: "tavus",
|
|
450
514
|
version: VERSION,
|
|
451
515
|
});
|
|
516
|
+
addToolIntent(server);
|
|
452
517
|
|
|
453
518
|
server.tool(
|
|
454
519
|
"tavus_mcp_health_check",
|
|
@@ -2520,12 +2585,20 @@ class TavusApiClient {
|
|
|
2520
2585
|
}
|
|
2521
2586
|
|
|
2522
2587
|
export function tavusApiRequestHeaders(apiKey: string): Record<string, string> {
|
|
2523
|
-
|
|
2588
|
+
const headers: Record<string, string> = {
|
|
2524
2589
|
"Content-Type": "application/json",
|
|
2525
2590
|
"User-Agent": `tavus-${CLIENT_SOURCE}/${CLIENT_VERSION}`,
|
|
2526
2591
|
"X-Tavus-Client-Source": CLIENT_SOURCE,
|
|
2527
2592
|
"x-api-key": apiKey,
|
|
2528
2593
|
};
|
|
2594
|
+
const toolCall = currentToolCall.getStore();
|
|
2595
|
+
if (toolCall) {
|
|
2596
|
+
headers["X-Tavus-Client-Tool"] = toolCall.tool;
|
|
2597
|
+
if (toolCall.intent) {
|
|
2598
|
+
headers["X-Tavus-Client-Intent"] = intentHeaderValue(toolCall.intent);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return headers;
|
|
2529
2602
|
}
|
|
2530
2603
|
|
|
2531
2604
|
async function firstStockFaceId(client: TavusApiClient): Promise<string> {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.5.0"; // x-release-please-version
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
handleOAuthRequest,
|
|
@@ -322,6 +323,51 @@ describe("Cloudflare Tavus MCP helpers", () => {
|
|
|
322
323
|
});
|
|
323
324
|
});
|
|
324
325
|
|
|
326
|
+
it("offers an optional intent argument on every hosted tool", () => {
|
|
327
|
+
const tools = registeredTools(createTavusMcpServer({ TAVUS_API_KEY: "secret" }));
|
|
328
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
329
|
+
const shape = (tool as unknown as { inputSchema: { shape: Record<string, z.ZodType> } })
|
|
330
|
+
.inputSchema.shape;
|
|
331
|
+
expect(shape.intent, `${name} intent`).toBeDefined();
|
|
332
|
+
expect(shape.intent.safeParse(undefined).success, `${name} intent optional`).toBe(true);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("tags API requests with the tool name and encoded intent", async () => {
|
|
337
|
+
const fetchMock = vi.fn(async () =>
|
|
338
|
+
new Response(JSON.stringify({ pal_id: "p1" }), {
|
|
339
|
+
status: 200,
|
|
340
|
+
headers: { "Content-Type": "application/json" },
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
344
|
+
try {
|
|
345
|
+
const tool = registeredTools(createTavusMcpServer({ TAVUS_API_KEY: "secret" }))
|
|
346
|
+
.tavus_pal_get;
|
|
347
|
+
await tool.handler({
|
|
348
|
+
pal_id: "p1",
|
|
349
|
+
include_settings: false,
|
|
350
|
+
intent: " Check the café PAL\nbefore 100% launch ",
|
|
351
|
+
});
|
|
352
|
+
await tool.handler({ pal_id: "p1", include_settings: false });
|
|
353
|
+
|
|
354
|
+
const [withIntent, withoutIntent] = fetchMock.mock.calls.map(
|
|
355
|
+
(call) => (call as unknown as [string, RequestInit])[1].headers as Record<string, string>,
|
|
356
|
+
);
|
|
357
|
+
expect(withIntent).toMatchObject({
|
|
358
|
+
"X-Tavus-Client-Tool": "tavus_pal_get",
|
|
359
|
+
"X-Tavus-Client-Intent": "Check the caf%C3%A9 PAL before 100%25 launch",
|
|
360
|
+
});
|
|
361
|
+
expect(decodeURIComponent(withIntent["X-Tavus-Client-Intent"])).toBe(
|
|
362
|
+
"Check the café PAL before 100% launch",
|
|
363
|
+
);
|
|
364
|
+
expect(withoutIntent["X-Tavus-Client-Tool"]).toBe("tavus_pal_get");
|
|
365
|
+
expect(withoutIntent).not.toHaveProperty("X-Tavus-Client-Intent");
|
|
366
|
+
} finally {
|
|
367
|
+
vi.unstubAllGlobals();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
325
371
|
it("identifies hosted MCP requests without exposing the API key in the user agent", () => {
|
|
326
372
|
expect(tavusApiRequestHeaders("secret")).toMatchObject({
|
|
327
373
|
"User-Agent": `tavus-mcp-worker/${VERSION}`,
|
|
@@ -718,6 +764,47 @@ describe("Cloudflare Tavus MCP helpers", () => {
|
|
|
718
764
|
});
|
|
719
765
|
});
|
|
720
766
|
|
|
767
|
+
it("allows Cursor DCR callbacks and canonicalizes the client name", async () => {
|
|
768
|
+
const response = await handleOAuthRequest(
|
|
769
|
+
new Request("https://mcp.tavus-preview.io/register", {
|
|
770
|
+
method: "POST",
|
|
771
|
+
headers: { "Content-Type": "application/json" },
|
|
772
|
+
body: JSON.stringify({
|
|
773
|
+
redirect_uris: ["https://www.cursor.com/agents/mcp/oauth/callback"],
|
|
774
|
+
client_name: "Attacker-controlled label",
|
|
775
|
+
}),
|
|
776
|
+
}),
|
|
777
|
+
{ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32) },
|
|
778
|
+
"/mcp",
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
expect(response?.status).toBe(201);
|
|
782
|
+
await expect(response?.json()).resolves.toMatchObject({
|
|
783
|
+
client_name: "Cursor",
|
|
784
|
+
redirect_uris: ["https://www.cursor.com/agents/mcp/oauth/callback"],
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("allows IPv6 loopback DCR callbacks", async () => {
|
|
789
|
+
const response = await handleOAuthRequest(
|
|
790
|
+
new Request("https://mcp.tavus-preview.io/register", {
|
|
791
|
+
method: "POST",
|
|
792
|
+
headers: { "Content-Type": "application/json" },
|
|
793
|
+
body: JSON.stringify({
|
|
794
|
+
redirect_uris: ["http://[::1]:53117/oauth/callback"],
|
|
795
|
+
client_name: "MCP Client",
|
|
796
|
+
}),
|
|
797
|
+
}),
|
|
798
|
+
{ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32) },
|
|
799
|
+
"/mcp",
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
expect(response?.status).toBe(201);
|
|
803
|
+
await expect(response?.json()).resolves.toMatchObject({
|
|
804
|
+
redirect_uris: ["http://[::1]:53117/oauth/callback"],
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
721
808
|
it("rejects arbitrary HTTPS dynamic client callbacks", async () => {
|
|
722
809
|
const response = await handleOAuthRequest(
|
|
723
810
|
new Request("https://mcp.tavus-preview.io/register", {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const VERSION = "0.4.4"; // x-release-please-version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/build-and-verify.md
RENAMED
|
File without changes
|
{tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/builder-workflow.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|