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.
Files changed (99) hide show
  1. tavus_cli-0.5.0/.release-please-manifest.json +3 -0
  2. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/CHANGELOG.md +14 -0
  3. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/PKG-INFO +1 -1
  4. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/usage-observability.md +29 -7
  5. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus/pyproject.toml +2 -2
  6. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus-mcp/pyproject.toml +2 -2
  7. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/pyproject.toml +1 -1
  8. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/__init__.py +1 -1
  9. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/client/http.py +41 -2
  10. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/server.py +48 -20
  11. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_mcp_server.py +35 -0
  12. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/uv.lock +1 -1
  13. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/package-lock.json +2 -2
  14. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/package.json +1 -1
  15. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/oauth.ts +14 -1
  16. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/tavus.ts +74 -1
  17. tavus_cli-0.5.0/workers/tavus-mcp/src/version.ts +1 -0
  18. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/test/tavus.test.ts +87 -0
  19. tavus_cli-0.4.4/.release-please-manifest.json +0 -3
  20. tavus_cli-0.4.4/workers/tavus-mcp/src/version.ts +0 -1
  21. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.env.example +0 -0
  22. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  23. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/deploy-cloudflare-worker.yml +0 -0
  24. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/pr-reviews.yml +0 -0
  25. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/pypi-cli.yml +0 -0
  26. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/tail-cloudflare-worker.yml +0 -0
  27. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.github/workflows/validate-staging-deployment.yml +0 -0
  28. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/.gitignore +0 -0
  29. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/AGENTS.md +0 -0
  30. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/README.md +0 -0
  31. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/README.md +0 -0
  32. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/listing.md +0 -0
  33. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/docs/gemini-enterprise/test-queries.md +0 -0
  34. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus/README.md +0 -0
  35. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/packages/tavus-mcp/README.md +0 -0
  36. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/release-please-config.json +0 -0
  37. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/scripts/__init__.py +0 -0
  38. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/scripts/check_release_versions.py +0 -0
  39. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/SKILL.md +0 -0
  40. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/agents/openai.yaml +0 -0
  41. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/build-and-verify.md +0 -0
  42. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/builder-workflow.md +0 -0
  43. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/chat-mode.md +0 -0
  44. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/skills/tavus-agentic-pals/references/examples.md +0 -0
  45. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/cli/__init__.py +0 -0
  46. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/cli/main.py +0 -0
  47. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/__init__.py +0 -0
  48. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/__init__.py +0 -0
  49. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/keyring_store.py +0 -0
  50. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/oauth.py +0 -0
  51. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/auth/session.py +0 -0
  52. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/client/__init__.py +0 -0
  53. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/daily_transport.py +0 -0
  54. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/env.py +0 -0
  55. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/errors.py +0 -0
  56. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/pal_urls.py +0 -0
  57. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/patch.py +0 -0
  58. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/__init__.py +0 -0
  59. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/build_and_verify.py +0 -0
  60. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/capabilities.py +0 -0
  61. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/carrier_builder.py +0 -0
  62. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/knowledge.py +0 -0
  63. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/options.py +0 -0
  64. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/quickstart.py +0 -0
  65. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/scaffold_embed.py +0 -0
  66. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/templates.py +0 -0
  67. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/recipes/tool_reuse.py +0 -0
  68. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/__init__.py +0 -0
  69. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/file_manifest.py +0 -0
  70. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/guardrail.py +0 -0
  71. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/objective.py +0 -0
  72. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/pal.py +0 -0
  73. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/pronunciation.py +0 -0
  74. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/src/tavus_mcp/sdk/schemas/tool.py +0 -0
  75. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/conftest.py +0 -0
  76. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_cli.py +0 -0
  77. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_client.py +0 -0
  78. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/integration/test_oauth.py +0 -0
  79. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_auth.py +0 -0
  80. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_build_and_verify.py +0 -0
  81. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_builder.py +0 -0
  82. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_capabilities.py +0 -0
  83. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_daily_transport.py +0 -0
  84. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_env.py +0 -0
  85. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_guardrail_schema.py +0 -0
  86. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_objective_schema.py +0 -0
  87. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_pal_schema.py +0 -0
  88. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_patch.py +0 -0
  89. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_pronunciation_schema.py +0 -0
  90. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_release_versions.py +0 -0
  91. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_scaffold_embed.py +0 -0
  92. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_tool_reuse.py +0 -0
  93. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/tests/unit/test_tool_schema.py +0 -0
  94. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/.dev.vars.example +0 -0
  95. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/index.ts +0 -0
  96. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/src/oauth-grant.ts +0 -0
  97. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/test/parity.test.ts +0 -0
  98. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/tsconfig.json +0 -0
  99. {tavus_cli-0.4.4 → tavus_cli-0.5.0}/workers/tavus-mcp/wrangler.jsonc +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.5.0"
3
+ }
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tavus-cli
3
- Version: 0.4.4
3
+ Version: 0.5.0
4
4
  Summary: Tavus CLI and MCP server for agentic PAL creation and editing.
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: fastmcp>=2.0.0
@@ -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. The
13
- header is available for future structured attribution; the user agent works with
14
- request-handler's current access logging today.
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
- - No API key, prompt, or request body is added to telemetry by this change. The
63
- source tags contain only the product surface and package version. Request-handler
64
- continues applying its existing API-key truncation and 64 KiB body-redaction
65
- policy.
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.4.4" # x-release-please-version
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.4.4", # x-release-please-version
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.4.4" # x-release-please-version
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.4.4", # x-release-please-version
8
+ "tavus-cli==0.5.0", # x-release-please-version
9
9
  ]
10
10
 
11
11
  [project.urls]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tavus-cli"
3
- version = "0.4.4"
3
+ version = "0.5.0"
4
4
  description = "Tavus CLI and MCP server for agentic PAL creation and editing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
1
  """Tavus CLI and MCP server package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.4.4" # x-release-please-version
4
+ __version__ = "0.5.0" # x-release-please-version
@@ -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
- return {
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 a conservative annotation policy to every registered Tavus tool."""
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 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)
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
@@ -1303,7 +1303,7 @@ wheels = [
1303
1303
 
1304
1304
  [[package]]
1305
1305
  name = "tavus-cli"
1306
- version = "0.4.4"
1306
+ version = "0.5.0"
1307
1307
  source = { editable = "." }
1308
1308
  dependencies = [
1309
1309
  { name = "fastmcp" },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@tavus/tavus-mcp-worker",
3
- "version": "0.4.4",
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.4.4",
9
+ "version": "0.5.0",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.29.0",
12
12
  "agents": "^0.12.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tavus/tavus-mcp-worker",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost");
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
- return {
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,3 +0,0 @@
1
- {
2
- ".": "0.4.4"
3
- }
@@ -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