tavus-cli 0.9.2__tar.gz → 0.9.3__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 (117) hide show
  1. tavus_cli-0.9.3/.release-please-manifest.json +3 -0
  2. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/CHANGELOG.md +7 -0
  3. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/PKG-INFO +1 -1
  4. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/packages/tavus/pyproject.toml +2 -2
  5. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/packages/tavus-mcp/pyproject.toml +2 -2
  6. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/plugins/tavus-pal/plugin.json +2 -1
  7. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/pyproject.toml +1 -1
  8. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/__init__.py +1 -1
  9. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/uv.lock +1 -1
  10. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/package-lock.json +2 -2
  11. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/package.json +1 -1
  12. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/src/oauth.ts +46 -3
  13. tavus_cli-0.9.3/workers/tavus-mcp/src/version.ts +1 -0
  14. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/test/tavus.test.ts +220 -0
  15. tavus_cli-0.9.2/.release-please-manifest.json +0 -3
  16. tavus_cli-0.9.2/workers/tavus-mcp/src/version.ts +0 -1
  17. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.agents/plugins/marketplace.json +0 -0
  18. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.env.example +0 -0
  19. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/ci.yml +0 -0
  20. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/deploy-cloudflare-worker.yml +0 -0
  21. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/pr-reviews.yml +0 -0
  22. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/pypi-cli.yml +0 -0
  23. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/tail-cloudflare-worker.yml +0 -0
  24. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.github/workflows/validate-staging-deployment.yml +0 -0
  25. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/.gitignore +0 -0
  26. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/AGENTS.md +0 -0
  27. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/PROMPT.md +0 -0
  28. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/README.md +0 -0
  29. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/builder-guidance.md +0 -0
  30. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/chatgpt-plugin.md +0 -0
  31. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/gemini-enterprise/README.md +0 -0
  32. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/gemini-enterprise/listing.md +0 -0
  33. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/gemini-enterprise/test-queries.md +0 -0
  34. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/docs/usage-observability.md +0 -0
  35. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/packages/tavus/README.md +0 -0
  36. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/packages/tavus-mcp/README.md +0 -0
  37. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/plugins/tavus-pal/assets/icon-dark.svg +0 -0
  38. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/plugins/tavus-pal/assets/icon.svg +0 -0
  39. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/plugins/tavus-pal/mcp.json +0 -0
  40. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/release-please-config.json +0 -0
  41. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/scripts/__init__.py +0 -0
  42. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/scripts/build_chatgpt_plugin.py +0 -0
  43. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/scripts/check_release_versions.py +0 -0
  44. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/SKILL.md +0 -0
  45. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/agents/openai.yaml +0 -0
  46. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/references/build-and-verify.md +0 -0
  47. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/references/builder-workflow.md +0 -0
  48. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/references/chat-mode.md +0 -0
  49. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/skills/tavus-agentic-pals/references/examples.md +0 -0
  50. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/builder_guidance.json +0 -0
  51. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/builder_guidance.py +0 -0
  52. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/cli/__init__.py +0 -0
  53. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/cli/main.py +0 -0
  54. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/__init__.py +0 -0
  55. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/auth/__init__.py +0 -0
  56. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/auth/keyring_store.py +0 -0
  57. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/auth/oauth.py +0 -0
  58. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/auth/session.py +0 -0
  59. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/client/__init__.py +0 -0
  60. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/client/http.py +0 -0
  61. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/daily_transport.py +0 -0
  62. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/env.py +0 -0
  63. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/errors.py +0 -0
  64. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/pal_urls.py +0 -0
  65. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/patch.py +0 -0
  66. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/__init__.py +0 -0
  67. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/build_and_verify.py +0 -0
  68. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/capabilities.py +0 -0
  69. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/carrier_builder.py +0 -0
  70. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/deployments.py +0 -0
  71. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/knowledge.py +0 -0
  72. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/options.py +0 -0
  73. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/quickstart.py +0 -0
  74. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/scaffold_embed.py +0 -0
  75. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/templates.py +0 -0
  76. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/recipes/tool_reuse.py +0 -0
  77. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/__init__.py +0 -0
  78. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/deployment.py +0 -0
  79. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/file_manifest.py +0 -0
  80. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/guardrail.py +0 -0
  81. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/objective.py +0 -0
  82. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/pal.py +0 -0
  83. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/pronunciation.py +0 -0
  84. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/sdk/schemas/tool.py +0 -0
  85. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/src/tavus_mcp/server.py +0 -0
  86. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/conftest.py +0 -0
  87. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/integration/test_cli.py +0 -0
  88. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/integration/test_client.py +0 -0
  89. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/integration/test_mcp_server.py +0 -0
  90. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/integration/test_oauth.py +0 -0
  91. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_auth.py +0 -0
  92. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_build_and_verify.py +0 -0
  93. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_builder.py +0 -0
  94. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_capabilities.py +0 -0
  95. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_daily_transport.py +0 -0
  96. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_deployments.py +0 -0
  97. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_env.py +0 -0
  98. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_guardrail_schema.py +0 -0
  99. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_objective_schema.py +0 -0
  100. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_pal_schema.py +0 -0
  101. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_patch.py +0 -0
  102. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_pronunciation_schema.py +0 -0
  103. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_release_versions.py +0 -0
  104. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_scaffold_embed.py +0 -0
  105. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_tool_reuse.py +0 -0
  106. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/tests/unit/test_tool_schema.py +0 -0
  107. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/.dev.vars.example +0 -0
  108. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/src/index.ts +0 -0
  109. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/src/oauth-grant.ts +0 -0
  110. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/src/oauth-session.ts +0 -0
  111. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/src/tavus.ts +0 -0
  112. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/test/builder-guidance.test.ts +0 -0
  113. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/test/oauth-session.test.ts +0 -0
  114. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/test/parity.test.ts +0 -0
  115. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/test/worker-runtime.ts +0 -0
  116. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/tsconfig.json +0 -0
  117. {tavus_cli-0.9.2 → tavus_cli-0.9.3}/workers/tavus-mcp/wrangler.jsonc +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.9.3"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.3](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.9.2...v0.9.3) (2026-10-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **worker:** accept any loopback port and legacy MCP client IDs at sign-in ([#70](https://github.com/Tavus-Engineering/tavus-mcp/issues/70)) ([231a42c](https://github.com/Tavus-Engineering/tavus-mcp/commit/231a42c39806646c21ae07af3330927dbba5ea4a))
9
+
3
10
  ## [0.9.2](https://github.com/Tavus-Engineering/tavus-mcp/compare/v0.9.1...v0.9.2) (2026-10-10)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tavus-cli
3
- Version: 0.9.2
3
+ Version: 0.9.3
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
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "tavus"
3
- version = "0.9.2" # x-release-please-version
3
+ version = "0.9.3" # 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.9.2", # x-release-please-version
8
+ "tavus-cli==0.9.3", # 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.9.2" # x-release-please-version
3
+ version = "0.9.3" # 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.9.2", # x-release-please-version
8
+ "tavus-cli==0.9.3", # x-release-please-version
9
9
  ]
10
10
 
11
11
  [project.urls]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "tavus-pal",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "description": "Create, edit, and test Tavus PALs: digital humans with a face, voice, knowledge, and conversational behavior. Uses the Tavus hosted MCP server and your connected Tavus account.",
6
6
  "author": {
7
7
  "name": "Tavus",
@@ -37,6 +37,7 @@
37
37
  "logoDark": "./assets/icon-dark.svg"
38
38
  },
39
39
  "review": {
40
+ "demo_recording_url": "https://tavus-mcp-review.pages.dev/tavus-openai-review-demo-2026-09-10-19f4128cd7a5cbc4.mp4",
40
41
  "commerce": false,
41
42
  "commerce_description": "Users access their existing Tavus account entitlements. This plugin does not sell PALs, subscriptions, or credits, collect payment details, or initiate checkout.",
42
43
  "test_cases": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tavus-cli"
3
- version = "0.9.2"
3
+ version = "0.9.3"
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.9.2" # x-release-please-version
4
+ __version__ = "0.9.3" # x-release-please-version
@@ -1303,7 +1303,7 @@ wheels = [
1303
1303
 
1304
1304
  [[package]]
1305
1305
  name = "tavus-cli"
1306
- version = "0.9.2"
1306
+ version = "0.9.3"
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.9.2",
3
+ "version": "0.9.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@tavus/tavus-mcp-worker",
9
- "version": "0.9.2",
9
+ "version": "0.9.3",
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.9.2",
3
+ "version": "0.9.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -18,6 +18,13 @@ const REGISTER_ROUTE = "/register";
18
18
  const CALLBACK_ROUTE = "/oauth/callback";
19
19
  const AUTHORIZATION_REQUEST_ROUTE = "/oauth/authorization-request";
20
20
  const DYNAMIC_CLIENT_PREFIX = "tavus-mcp-dcr";
21
+ // Client IDs that /register issued before 2026-08-06 (#21). The Worker never
22
+ // stored them, and /authorize accepted any client ID whose redirect was a
23
+ // loopback URL. Clients such as Claude Code keep replaying the ID they cached
24
+ // instead of registering again (anthropics/claude-code#84614), and the
25
+ // rejection they get is a browser page they never see. Keeping the old rule
26
+ // for these IDs, loopback only, lets them sign in without any stored state.
27
+ const LEGACY_DYNAMIC_CLIENT_ID = /^tavus-mcp-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/;
21
28
  const AUTHORIZATION_STATE_TTL_SECONDS = 10 * 60;
22
29
  const AUTHORIZATION_CODE_TTL_SECONDS = 5 * 60;
23
30
 
@@ -191,10 +198,37 @@ function findRegisteredRedirect(
191
198
  redirectUri: string,
192
199
  ): RegisteredOAuthClient | undefined {
193
200
  return clients.find(
194
- (c) => c.client_id === clientId && c.redirect_uris.includes(redirectUri),
201
+ (c) =>
202
+ c.client_id === clientId &&
203
+ c.redirect_uris.some((registered) => redirectUriMatches(registered, redirectUri)),
195
204
  );
196
205
  }
197
206
 
207
+ // RFC 8252 section 7.3: a native client takes an ephemeral port when it starts
208
+ // the flow, so the authorization server MUST accept any port on a loopback
209
+ // redirect URI. Everything else about the URI still has to match. HTTPS
210
+ // callbacks need an exact match.
211
+ function redirectUriMatches(registered: string, requested: string): boolean {
212
+ if (registered === requested) {
213
+ return true;
214
+ }
215
+ if (!isLoopbackUrl(registered) || !isLoopbackUrl(requested)) {
216
+ return false;
217
+ }
218
+ const a = new URL(registered);
219
+ const b = new URL(requested);
220
+ return (
221
+ a.protocol === b.protocol &&
222
+ a.hostname === b.hostname &&
223
+ a.pathname === b.pathname &&
224
+ a.search === b.search
225
+ );
226
+ }
227
+
228
+ function isLegacyDynamicClient(clientId: string): boolean {
229
+ return LEGACY_DYNAMIC_CLIENT_ID.test(clientId);
230
+ }
231
+
198
232
  async function resolveRegisteredRedirect(
199
233
  env: Env,
200
234
  clientId: string,
@@ -208,7 +242,7 @@ async function resolveRegisteredRedirect(
208
242
  const signingSecret = oauthSigningSecret(env);
209
243
  if (signingSecret) {
210
244
  const dynamic = await verifyClientPayload(clientId, signingSecret);
211
- if (dynamic?.redirect_uris.includes(redirectUri)) {
245
+ if (dynamic?.redirect_uris.some((registered) => redirectUriMatches(registered, redirectUri))) {
212
246
  return {
213
247
  client_id: clientId,
214
248
  redirect_uris: dynamic.redirect_uris,
@@ -217,6 +251,10 @@ async function resolveRegisteredRedirect(
217
251
  }
218
252
  }
219
253
 
254
+ if (isLegacyDynamicClient(clientId) && isLoopbackUrl(redirectUri)) {
255
+ return { client_id: clientId, redirect_uris: [redirectUri], name: "Tavus MCP Client" };
256
+ }
257
+
220
258
  return undefined;
221
259
  }
222
260
 
@@ -378,9 +416,10 @@ async function handleAuthorize(request: Request, env: Env, mcpRoute: string): Pr
378
416
 
379
417
  const registeredRedirect = await resolveRegisteredRedirect(env, clientId, redirectUri);
380
418
  if (!registeredRedirect) {
419
+ // This page is all the person sees; the MCP client never learns about it.
381
420
  return oauthError(
382
421
  "invalid_request",
383
- "redirect_uri is not registered for this client.",
422
+ "redirect_uri is not registered for this client. Remove and re-add the Tavus MCP server in your client so it registers again.",
384
423
  400,
385
424
  );
386
425
  }
@@ -654,6 +693,10 @@ async function authenticateClient(request: Request, params: URLSearchParams, env
654
693
  }
655
694
  return secret ? null : clientId;
656
695
  }
696
+ // Legacy IDs were always public clients; see LEGACY_DYNAMIC_CLIENT_ID.
697
+ if (isLegacyDynamicClient(clientId)) {
698
+ return !secret && !basic ? clientId : null;
699
+ }
657
700
  const signingSecret = oauthSigningSecret(env);
658
701
  const dynamic = signingSecret ? await verifyClientPayload(clientId, signingSecret) : null;
659
702
  if (!dynamic || !signingSecret) return null;
@@ -0,0 +1 @@
1
+ export const VERSION = "0.9.3"; // x-release-please-version
@@ -1017,6 +1017,226 @@ describe("Cloudflare Tavus MCP helpers", () => {
1017
1017
  expect(response?.status).toBe(400);
1018
1018
  });
1019
1019
 
1020
+ function authorizeUrl(clientId: string, redirectUri: string): URL {
1021
+ const url = new URL("https://mcp.tavus-preview.io/authorize");
1022
+ url.search = new URLSearchParams({
1023
+ response_type: "code",
1024
+ client_id: clientId,
1025
+ redirect_uri: redirectUri,
1026
+ state: "state-token",
1027
+ code_challenge: "challenge",
1028
+ code_challenge_method: "S256",
1029
+ resource: "https://mcp.tavus-preview.io/mcp",
1030
+ scope: "tavus",
1031
+ }).toString();
1032
+ return url;
1033
+ }
1034
+
1035
+ async function registerLoopbackClient(env: Env, redirectUri: string): Promise<string> {
1036
+ const registration = await handleOAuthRequest(
1037
+ new Request("https://mcp.tavus-preview.io/register", {
1038
+ method: "POST",
1039
+ headers: { "Content-Type": "application/json" },
1040
+ body: JSON.stringify({ redirect_uris: [redirectUri], client_name: "Claude Code" }),
1041
+ }),
1042
+ env,
1043
+ "/mcp",
1044
+ );
1045
+ expect(registration?.status).toBe(201);
1046
+ return ((await registration?.json()) as { client_id: string }).client_id;
1047
+ }
1048
+
1049
+ // RFC 8252 section 7.3: loopback clients take an ephemeral port per attempt.
1050
+ it("accepts any loopback port for a dynamically registered callback", async () => {
1051
+ const env = withOAuthGrants({
1052
+ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32),
1053
+ TAVUS_DEV_PORTAL_URL: "https://platform.example.test",
1054
+ });
1055
+ const clientId = await registerLoopbackClient(env, "http://localhost:4000/callback");
1056
+
1057
+ const response = await handleOAuthRequest(
1058
+ new Request(authorizeUrl(clientId, "http://localhost:53117/callback")),
1059
+ env,
1060
+ "/mcp",
1061
+ );
1062
+
1063
+ expect(response?.status).toBe(302);
1064
+ const location = new URL(response?.headers.get("Location") ?? "");
1065
+ expect(location.origin + location.pathname).toBe("https://platform.example.test/dev/cli-authorize");
1066
+ });
1067
+
1068
+ it.each([
1069
+ ["a different path", "http://localhost:4000/other"],
1070
+ ["a different loopback host", "http://127.0.0.1:4000/callback"],
1071
+ ["an added query string", "http://localhost:4000/callback?next=https://attacker.example"],
1072
+ ["an HTTPS callback", "https://example.test/callback"],
1073
+ ])("rejects a dynamically registered loopback client's callback with %s", async (_label, redirectUri) => {
1074
+ const env = withOAuthGrants({ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32) });
1075
+ const clientId = await registerLoopbackClient(env, "http://localhost:4000/callback");
1076
+
1077
+ const response = await handleOAuthRequest(new Request(authorizeUrl(clientId, redirectUri)), env, "/mcp");
1078
+
1079
+ expect(response?.status).toBe(400);
1080
+ });
1081
+
1082
+ it("accepts any loopback port for a pre-registered loopback callback", async () => {
1083
+ const env = withOAuthGrants({
1084
+ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32),
1085
+ MCP_OAUTH_CLIENTS: JSON.stringify([
1086
+ { client_id: "static-cli", redirect_uris: ["http://127.0.0.1:4000/callback"] },
1087
+ ]),
1088
+ });
1089
+
1090
+ const response = await handleOAuthRequest(
1091
+ new Request(authorizeUrl("static-cli", "http://127.0.0.1:4001/callback")),
1092
+ env,
1093
+ "/mcp",
1094
+ );
1095
+
1096
+ expect(response?.status).toBe(302);
1097
+ });
1098
+
1099
+ it("still needs an exact match for a pre-registered HTTPS callback", async () => {
1100
+ const env = withOAuthGrants({
1101
+ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32),
1102
+ MCP_OAUTH_CLIENTS: JSON.stringify([
1103
+ { client_id: "static-web", redirect_uris: ["https://example.test/callback"] },
1104
+ ]),
1105
+ });
1106
+
1107
+ const response = await handleOAuthRequest(
1108
+ new Request(authorizeUrl("static-web", "https://example.test:8443/callback")),
1109
+ env,
1110
+ "/mcp",
1111
+ );
1112
+
1113
+ expect(response?.status).toBe(400);
1114
+ });
1115
+
1116
+ // Issued by /register before #21; the Worker never stored them.
1117
+ const legacyClientId = "tavus-mcp-db38e103-83e8-4046-87d6-026739d5133c";
1118
+
1119
+ it("accepts a legacy dynamic client ID with a loopback callback", async () => {
1120
+ const env = withOAuthGrants({
1121
+ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32),
1122
+ TAVUS_DEV_PORTAL_URL: "https://platform.example.test",
1123
+ });
1124
+
1125
+ const response = await handleOAuthRequest(
1126
+ new Request(authorizeUrl(legacyClientId, "http://localhost:3118/callback")),
1127
+ env,
1128
+ "/mcp",
1129
+ );
1130
+
1131
+ expect(response?.status).toBe(302);
1132
+ const location = new URL(response?.headers.get("Location") ?? "");
1133
+ const requestInfoUrl = new URL("https://mcp.tavus-preview.io/oauth/authorization-request");
1134
+ requestInfoUrl.searchParams.set("state", location.searchParams.get("state")!);
1135
+ const requestInfo = await handleOAuthRequest(
1136
+ new Request(requestInfoUrl, { headers: { Origin: "https://platform.example.test" } }),
1137
+ env,
1138
+ "/mcp",
1139
+ );
1140
+ await expect(requestInfo?.json()).resolves.toMatchObject({
1141
+ client_name: "Tavus MCP Client",
1142
+ redirect_uri: "http://localhost:3118/callback",
1143
+ });
1144
+ });
1145
+
1146
+ it("rejects a legacy dynamic client ID with a non-loopback callback", async () => {
1147
+ const env = withOAuthGrants({ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32) });
1148
+
1149
+ const response = await handleOAuthRequest(
1150
+ new Request(authorizeUrl(legacyClientId, "https://attacker.example/callback")),
1151
+ env,
1152
+ "/mcp",
1153
+ );
1154
+
1155
+ expect(response?.status).toBe(400);
1156
+ });
1157
+
1158
+ it.each([
1159
+ ["a signed-looking ID with a bad signature", "tavus-mcp-dcr.bad.sig"],
1160
+ ["an unknown ID", "random-client"],
1161
+ ["a legacy-looking ID that is not a UUID", "tavus-mcp-not-a-uuid"],
1162
+ ])("tells the person how to recover when %s is used", async (_label, clientId) => {
1163
+ const env = withOAuthGrants({ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32) });
1164
+
1165
+ const response = await handleOAuthRequest(
1166
+ new Request(authorizeUrl(clientId, "http://localhost:3118/callback")),
1167
+ env,
1168
+ "/mcp",
1169
+ );
1170
+
1171
+ expect(response?.status).toBe(400);
1172
+ await expect(response?.json()).resolves.toEqual({
1173
+ error: "invalid_request",
1174
+ error_description:
1175
+ "redirect_uri is not registered for this client. Remove and re-add the Tavus MCP server in your client so it registers again.",
1176
+ });
1177
+ });
1178
+
1179
+ it("exchanges a legacy dynamic client's code as a public client only", async () => {
1180
+ const env = withOAuthGrants({
1181
+ MCP_OAUTH_SIGNING_SECRET: "s".repeat(32),
1182
+ TAVUS_DEV_PORTAL_URL: "https://platform.example.test",
1183
+ });
1184
+ const redirectUri = "http://localhost:3118/callback";
1185
+ const resource = "https://mcp.tavus-preview.io/mcp";
1186
+ const verifier = "legacy-verifier-0123456789abcdefghijklmnopqrstuvwxyz";
1187
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1188
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
1189
+ .replace(/\+/g, "-")
1190
+ .replace(/\//g, "_")
1191
+ .replace(/=+$/g, "");
1192
+
1193
+ const authorizationUrl = authorizeUrl(legacyClientId, redirectUri);
1194
+ authorizationUrl.searchParams.set("code_challenge", challenge);
1195
+ const authorization = await handleOAuthRequest(new Request(authorizationUrl), env, "/mcp");
1196
+ const portalUrl = new URL(authorization?.headers.get("Location") ?? "");
1197
+ const callback = await handleOAuthRequest(
1198
+ new Request("https://mcp.tavus-preview.io/oauth/callback", {
1199
+ method: "POST",
1200
+ headers: {
1201
+ Accept: "application/json",
1202
+ "Content-Type": "application/x-www-form-urlencoded",
1203
+ Origin: "https://platform.example.test",
1204
+ },
1205
+ body: new URLSearchParams({ code: "tavus-user-key", state: portalUrl.searchParams.get("state")! }),
1206
+ }),
1207
+ env,
1208
+ "/mcp",
1209
+ );
1210
+ const callbackResult = (await callback?.json()) as { redirect_uri: string };
1211
+ const clientUrl = new URL(callbackResult.redirect_uri);
1212
+ expect(clientUrl.origin + clientUrl.pathname).toBe(redirectUri);
1213
+
1214
+ const tokenRequest = (withSecret: boolean) =>
1215
+ new Request("https://mcp.tavus-preview.io/token", {
1216
+ method: "POST",
1217
+ body: new URLSearchParams({
1218
+ grant_type: "authorization_code",
1219
+ code: clientUrl.searchParams.get("code")!,
1220
+ client_id: legacyClientId,
1221
+ ...(withSecret ? { client_secret: "guess" } : {}),
1222
+ redirect_uri: redirectUri,
1223
+ code_verifier: verifier,
1224
+ resource,
1225
+ }),
1226
+ });
1227
+ const withSecret = await handleOAuthRequest(tokenRequest(true), env, "/mcp");
1228
+ expect(withSecret?.status).toBe(401);
1229
+ await expect(withSecret?.json()).resolves.toMatchObject({ error: "invalid_client" });
1230
+
1231
+ const token = await handleOAuthRequest(tokenRequest(false), env, "/mcp");
1232
+ expect(token?.status).toBe(200);
1233
+ await expect(token?.json()).resolves.toMatchObject({
1234
+ access_token: expect.stringMatching(/^tavus_mcp_access\./),
1235
+ token_type: "Bearer",
1236
+ scope: "tavus",
1237
+ });
1238
+ });
1239
+
1020
1240
  it("validates client_secret before exchanging a pre-registered client code", async () => {
1021
1241
  const env = {
1022
1242
  MCP_OAUTH_CLIENTS: JSON.stringify([
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.9.2"
3
- }
@@ -1 +0,0 @@
1
- export const VERSION = "0.9.2"; // 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