tavus-cli 0.1.0__py3-none-any.whl

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_mcp/server.py ADDED
@@ -0,0 +1,877 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastmcp import FastMCP
6
+
7
+ from tavus_mcp.sdk.client.http import TavusClient
8
+ from tavus_mcp.sdk.recipes.build_and_verify import build_and_verify
9
+ from tavus_mcp.sdk.recipes.options import describe_persona_options
10
+ from tavus_mcp.sdk.recipes.quickstart import quickstart
11
+ from tavus_mcp.sdk.recipes.scaffold_embed import EmbedTarget, scaffold_embed
12
+ from tavus_mcp.sdk.recipes.templates import PersonaTemplate, persona_from_template
13
+ from tavus_mcp.sdk.recipes.tool_reuse import (
14
+ build_inline_deprecation_notice,
15
+ build_library_match_advisory,
16
+ collect_inline_tools,
17
+ )
18
+
19
+ mcp = FastMCP(
20
+ "tavus",
21
+ instructions=(
22
+ "Persona tools are first-class objects, not inlined on the persona. "
23
+ "Before giving a persona a tool, check the saved library with "
24
+ "tavus_tool_list or tavus_describe_persona_options, then either ATTACH an "
25
+ "existing tool (tavus_persona_tools_attach) or CREATE a new one "
26
+ "(tavus_tool_create) and attach it. Writing tools inline via "
27
+ "tavus_patch_persona (layers.*.tools) is deprecated. Whenever you create a "
28
+ "new tool that overlaps an existing one, tell the user which existing tool "
29
+ "you saw and why its shape does not fit."
30
+ ),
31
+ )
32
+
33
+
34
+ def _list_data(value: Any) -> list[Any]:
35
+ if isinstance(value, dict) and isinstance(value.get("data"), list):
36
+ return value["data"]
37
+ if isinstance(value, list):
38
+ return value
39
+ return []
40
+
41
+
42
+ @mcp.tool()
43
+ async def tavus_persona_list(
44
+ limit: int = 25,
45
+ page: int = 0,
46
+ persona_type: str | None = None,
47
+ ) -> Any:
48
+ async with TavusClient.from_env() as client:
49
+ return await client.personas.list(limit=limit, page=page, persona_type=persona_type)
50
+
51
+
52
+ @mcp.tool()
53
+ async def tavus_persona_get(persona_id: str, include_settings: bool = False) -> Any:
54
+ async with TavusClient.from_env() as client:
55
+ return await client.personas.get(persona_id, include_settings=str(include_settings).lower())
56
+
57
+
58
+ @mcp.tool()
59
+ async def tavus_persona_create(
60
+ system_prompt: str | None = None,
61
+ persona_name: str = "Agentic Tavus Persona",
62
+ default_replica_id: str | None = None,
63
+ pipeline_mode: str = "full",
64
+ greeting: str | None = None,
65
+ context: str | None = None,
66
+ layers: dict[str, Any] | None = None,
67
+ memories: list[str] | None = None,
68
+ objectives_id: str | None = None,
69
+ guardrails_id: str | None = None,
70
+ guardrail_ids: list[str] | None = None,
71
+ guardrail_tags: list[str] | None = None,
72
+ document_ids: list[str] | None = None,
73
+ document_tags: list[str] | None = None,
74
+ is_template: bool | None = None,
75
+ ) -> Any:
76
+ """Create a persona. Beyond ``system_prompt``/``persona_name``, you can seed:
77
+ - ``pipeline_mode`` — ``"full"`` (default; LLM + TTS + STT + perception),
78
+ ``"speech-to-speech"`` (no LLM), or ``"echo"`` (TTS + transport only).
79
+ ``system_prompt`` is required for ``full`` and forbidden for the others.
80
+ - ``greeting`` — opening line the replica speaks.
81
+ - ``context`` — opaque text appended to the system prompt at runtime.
82
+ - ``layers`` — per-layer overrides (llm/tts/stt/perception/conversational_flow/
83
+ capabilities/knowledge_base). Same shape RQH stores; use
84
+ ``tavus_describe_persona_options`` to see valid paths.
85
+ - ``memories`` — initial seed lines for the persona's memory store.
86
+ - ``objectives_id`` — attach an existing objective set.
87
+ - ``guardrails_id`` (legacy set) or ``guardrail_ids`` / ``guardrail_tags``
88
+ (flat per-rule shape) — attach guardrails.
89
+ - ``document_ids`` / ``document_tags`` — RAG knowledge base.
90
+ - ``is_template`` — flag this persona as a template others can clone.
91
+
92
+ To attach **tools**, create the persona first then call
93
+ ``tavus_persona_tools_attach`` — tools live in a separate junction,
94
+ not at persona-create time."""
95
+ body: dict[str, Any] = {"persona_name": persona_name}
96
+ if pipeline_mode and pipeline_mode != "full":
97
+ body["pipeline_mode"] = pipeline_mode
98
+ if system_prompt is not None:
99
+ body["system_prompt"] = system_prompt
100
+ if default_replica_id:
101
+ body["default_replica_id"] = default_replica_id
102
+ if greeting is not None:
103
+ body["greeting"] = greeting
104
+ if context is not None:
105
+ body["context"] = context
106
+ if layers is not None:
107
+ body["layers"] = layers
108
+ if memories is not None:
109
+ body["memories"] = memories
110
+ if objectives_id:
111
+ body["objectives_id"] = objectives_id
112
+ if guardrails_id:
113
+ body["guardrails_id"] = guardrails_id
114
+ if guardrail_ids:
115
+ body["guardrail_ids"] = guardrail_ids
116
+ if guardrail_tags:
117
+ body["guardrail_tags"] = guardrail_tags
118
+ if document_ids:
119
+ body["document_ids"] = document_ids
120
+ if document_tags:
121
+ body["document_tags"] = document_tags
122
+ if is_template is not None:
123
+ body["is_template"] = is_template
124
+ async with TavusClient.from_env() as client:
125
+ return await client.personas.create(body)
126
+
127
+
128
+ @mcp.tool()
129
+ async def tavus_persona_delete(persona_id: str) -> Any:
130
+ async with TavusClient.from_env() as client:
131
+ return await client.personas.delete(persona_id)
132
+
133
+
134
+ @mcp.tool()
135
+ async def tavus_patch_persona(persona_id: str, ops: list[dict[str, Any]]) -> Any:
136
+ """Patch a persona with JSON Patch operations.
137
+
138
+ Tools are first-class now: prefer tavus_tool_create + tavus_persona_tools_attach
139
+ over writing tools inline here. If any op writes inline tools (layers.*.tools),
140
+ the result carries `_inline_tools_deprecated` steering you to that flow."""
141
+ inline = collect_inline_tools(ops)
142
+ async with TavusClient.from_env() as client:
143
+ result = await client.personas.patch(persona_id, ops)
144
+ if inline and isinstance(result, dict):
145
+ result["_inline_tools_deprecated"] = build_inline_deprecation_notice(inline)
146
+ return result
147
+
148
+
149
+ @mcp.tool()
150
+ async def tavus_describe_persona_options(persona_id: str) -> Any:
151
+ async with TavusClient.from_env() as client:
152
+ return await describe_persona_options(client, persona_id)
153
+
154
+
155
+ @mcp.tool()
156
+ async def tavus_replica_list(limit: int = 25, stock: bool = False) -> Any:
157
+ async with TavusClient.from_env() as client:
158
+ return await client.replicas.list(limit=limit, replica_type="system" if stock else None)
159
+
160
+
161
+ @mcp.tool()
162
+ async def tavus_conversation_create(
163
+ persona_id: str | None = None,
164
+ replica_id: str | None = None,
165
+ conversation_name: str | None = None,
166
+ ) -> Any:
167
+ body = {
168
+ "persona_id": persona_id,
169
+ "replica_id": replica_id,
170
+ "conversation_name": conversation_name,
171
+ }
172
+ async with TavusClient.from_env() as client:
173
+ return await client.conversations.create({k: v for k, v in body.items() if v})
174
+
175
+
176
+ @mcp.tool()
177
+ async def tavus_conversation_end(conversation_id: str) -> Any:
178
+ async with TavusClient.from_env() as client:
179
+ return await client.conversations.end(conversation_id)
180
+
181
+
182
+ @mcp.tool()
183
+ async def tavus_quickstart(
184
+ system_prompt: str,
185
+ persona_name: str = "Agentic Tavus Persona",
186
+ replica_id: str | None = None,
187
+ ) -> Any:
188
+ async with TavusClient.from_env() as client:
189
+ return await quickstart(
190
+ client,
191
+ system_prompt=system_prompt,
192
+ persona_name=persona_name,
193
+ replica_id=replica_id,
194
+ )
195
+
196
+
197
+ @mcp.tool()
198
+ async def tavus_persona_from_template(
199
+ template: PersonaTemplate,
200
+ persona_name: str | None = None,
201
+ business_context: str | None = None,
202
+ default_replica_id: str | None = None,
203
+ layers: dict[str, Any] | None = None,
204
+ ) -> Any:
205
+ async with TavusClient.from_env() as client:
206
+ return await persona_from_template(
207
+ client,
208
+ template=template,
209
+ persona_name=persona_name,
210
+ business_context=business_context,
211
+ default_replica_id=default_replica_id,
212
+ layers=layers,
213
+ )
214
+
215
+
216
+ @mcp.tool()
217
+ async def tavus_scaffold_embed(
218
+ conversation_url: str,
219
+ target: EmbedTarget = "iframe",
220
+ component_name: str = "TavusConversation",
221
+ ) -> Any:
222
+ return scaffold_embed(
223
+ conversation_url=conversation_url,
224
+ target=target,
225
+ component_name=component_name,
226
+ ).model_dump()
227
+
228
+
229
+ @mcp.tool()
230
+ async def tavus_resource_list(resource: str, limit: int = 25) -> Any:
231
+ async with TavusClient.from_env() as client:
232
+ return await _resource(client, resource).list(limit=limit)
233
+
234
+
235
+ @mcp.tool()
236
+ async def tavus_guardrail_list(
237
+ limit: int = 25,
238
+ page: int = 1,
239
+ type: str = "user",
240
+ name_or_uuid: str | None = None,
241
+ tags: str | None = None,
242
+ legacy: bool = False,
243
+ verbose: bool = False,
244
+ ) -> Any:
245
+ """List guardrails. `type` is "user" (account), "system", or "all";
246
+ `legacy=False` returns the flat, per-rule shape used by the new
247
+ persona-builder UI; `verbose=True` adds `persona_refs` and `guardrail_type`."""
248
+ params: dict[str, Any] = {
249
+ "limit": limit,
250
+ "page": page,
251
+ "type": type,
252
+ "legacy": "true" if legacy else "false",
253
+ }
254
+ if name_or_uuid:
255
+ params["name_or_uuid"] = name_or_uuid
256
+ if tags:
257
+ params["tags"] = tags
258
+ if verbose:
259
+ params["verbose"] = "true"
260
+ async with TavusClient.from_env() as client:
261
+ return await client.guardrails.list(**params)
262
+
263
+
264
+ @mcp.tool()
265
+ async def tavus_guardrail_get(
266
+ guardrail_id: str, verbose: bool = False, legacy: bool | None = None
267
+ ) -> Any:
268
+ """Get a single guardrail. `verbose=True` adds `persona_refs` and, for
269
+ individual items, `guardrail_type`."""
270
+ params: dict[str, Any] = {}
271
+ if verbose:
272
+ params["verbose"] = "true"
273
+ if legacy is not None:
274
+ params["legacy"] = "true" if legacy else "false"
275
+ async with TavusClient.from_env() as client:
276
+ return await client.guardrails.get(guardrail_id, **params)
277
+
278
+
279
+ @mcp.tool()
280
+ async def tavus_guardrail_create(
281
+ guardrail_name: str,
282
+ guardrail_prompt: str,
283
+ modality: str = "verbal",
284
+ callback_url: str = "",
285
+ tool_call: dict[str, Any] | None = None,
286
+ app_message: bool = True,
287
+ tags: list[str] | None = None,
288
+ ) -> Any:
289
+ """Create a flat (non-set) guardrail. `modality` is "verbal" / "visual" /
290
+ "audio". `tool_call` is optional structured payload sent when the
291
+ guardrail fires; `app_message=True` emits a data-channel message."""
292
+ body: dict[str, Any] = {
293
+ "guardrail_name": guardrail_name,
294
+ "guardrail_prompt": guardrail_prompt,
295
+ "modality": modality,
296
+ "callback_url": callback_url,
297
+ "app_message": app_message,
298
+ "tags": tags or [],
299
+ }
300
+ if tool_call is not None:
301
+ body["tool_call"] = tool_call
302
+ async with TavusClient.from_env() as client:
303
+ return await client.guardrails.create(body)
304
+
305
+
306
+ @mcp.tool()
307
+ async def tavus_guardrail_patch(
308
+ guardrail_id: str,
309
+ guardrail_name: str | None = None,
310
+ guardrail_prompt: str | None = None,
311
+ modality: str | None = None,
312
+ callback_url: str | None = None,
313
+ tool_call: dict[str, Any] | None = None,
314
+ app_message: bool | None = None,
315
+ tags: list[str] | None = None,
316
+ ) -> Any:
317
+ """Update a guardrail. Omit fields to leave them unchanged. RQH replaces
318
+ the whole field where supplied (no JSON-Patch semantics)."""
319
+ body: dict[str, Any] = {}
320
+ if guardrail_name is not None:
321
+ body["guardrail_name"] = guardrail_name
322
+ if guardrail_prompt is not None:
323
+ body["guardrail_prompt"] = guardrail_prompt
324
+ if modality is not None:
325
+ body["modality"] = modality
326
+ if callback_url is not None:
327
+ body["callback_url"] = callback_url
328
+ if tool_call is not None:
329
+ body["tool_call"] = tool_call
330
+ if app_message is not None:
331
+ body["app_message"] = app_message
332
+ if tags is not None:
333
+ body["tags"] = tags
334
+ async with TavusClient.from_env() as client:
335
+ return await client.guardrails.patch(guardrail_id, body)
336
+
337
+
338
+ @mcp.tool()
339
+ async def tavus_guardrail_delete(guardrail_id: str) -> Any:
340
+ async with TavusClient.from_env() as client:
341
+ return await client.guardrails.delete(guardrail_id)
342
+
343
+
344
+ @mcp.tool()
345
+ async def tavus_guardrail_tags(
346
+ search: str | None = None,
347
+ page: int | None = None,
348
+ limit: int | None = None,
349
+ ) -> Any:
350
+ """List tags applied to the account's guardrails. Mirrors the documents
351
+ `/tags` endpoint shape (`{tags, total_count}`)."""
352
+ async with TavusClient.from_env() as client:
353
+ return await client.guardrails.tags(search=search, page=page, limit=limit)
354
+
355
+
356
+ @mcp.tool()
357
+ async def tavus_objective_list(
358
+ limit: int = 25,
359
+ page: int = 1,
360
+ type: str = "user",
361
+ name_or_uuid: str | None = None,
362
+ sort: str = "ascending",
363
+ ) -> Any:
364
+ """List objective *sets*. Objectives still bundle into named sets that
365
+ personas reference by ``objectives_id`` — unlike guardrails, sets were
366
+ not flattened."""
367
+ params: dict[str, Any] = {
368
+ "limit": limit,
369
+ "page": page,
370
+ "type": type,
371
+ "sort": sort,
372
+ }
373
+ if name_or_uuid:
374
+ params["name_or_uuid"] = name_or_uuid
375
+ async with TavusClient.from_env() as client:
376
+ return await client.objectives.list(**params)
377
+
378
+
379
+ @mcp.tool()
380
+ async def tavus_objective_get(objectives_id: str) -> Any:
381
+ async with TavusClient.from_env() as client:
382
+ return await client.objectives.get(objectives_id)
383
+
384
+
385
+ @mcp.tool()
386
+ async def tavus_objective_create(
387
+ data: list[dict[str, Any]],
388
+ name: str = "",
389
+ allow_loops: bool = False,
390
+ ) -> Any:
391
+ """Create an objective set. ``data`` is a list of objective items —
392
+ each carries ``objective_name`` (letters/digits/underscores only),
393
+ ``objective_prompt``, optional ``confirmation_mode`` ("auto" | "manual"),
394
+ ``modality`` ("verbal" | "visual" | "audio"), ``output_variables``,
395
+ ``callback_url``, ``tool_call``, and chains either via
396
+ ``next_required_objective`` (linear) **or** ``next_conditional_objectives``
397
+ (branching) — not both. Exactly one item must be the root (not
398
+ referenced by any other) unless ``allow_loops=true``."""
399
+ body = {"name": name, "data": data, "allow_loops": allow_loops}
400
+ async with TavusClient.from_env() as client:
401
+ return await client.objectives.create(body)
402
+
403
+
404
+ @mcp.tool()
405
+ async def tavus_objective_patch(objectives_id: str, ops: list[dict[str, Any]]) -> Any:
406
+ """Patch an objective set with JSON Patch ops. RQH applies them to the
407
+ set document and re-runs cycle/single-root validation on the result.
408
+ Common ops:
409
+ - ``{"op": "replace", "path": "/data/0/objective_prompt", "value": "..."}``
410
+ - ``{"op": "add", "path": "/data/-", "value": {<new objective>}}``
411
+ - ``{"op": "remove", "path": "/data/2"}``"""
412
+ async with TavusClient.from_env() as client:
413
+ return await client.objectives.patch(objectives_id, ops)
414
+
415
+
416
+ @mcp.tool()
417
+ async def tavus_objective_delete(objectives_id: str) -> Any:
418
+ async with TavusClient.from_env() as client:
419
+ return await client.objectives.delete(objectives_id)
420
+
421
+
422
+ @mcp.tool()
423
+ async def tavus_objective_validate(
424
+ data: list[dict[str, Any]],
425
+ name: str = "",
426
+ allow_loops: bool = False,
427
+ ) -> Any:
428
+ """Validate an objective-set payload (cycles, single root, references)
429
+ without persisting. Useful before bulk-importing or after a manual edit."""
430
+ body = {"name": name, "data": data, "allow_loops": allow_loops}
431
+ async with TavusClient.from_env() as client:
432
+ return await client.objectives.validate(body)
433
+
434
+
435
+ @mcp.tool()
436
+ async def tavus_tool_list(
437
+ limit: int = 25,
438
+ page: int = 1,
439
+ type: str = "user",
440
+ name_or_uuid: str | None = None,
441
+ sort: str = "ascending",
442
+ ) -> Any:
443
+ """List tools. `type` is "user" / "system" / "all"."""
444
+ params: dict[str, Any] = {
445
+ "limit": limit,
446
+ "page": page,
447
+ "type": type,
448
+ "sort": sort,
449
+ }
450
+ if name_or_uuid:
451
+ params["name_or_uuid"] = name_or_uuid
452
+ async with TavusClient.from_env() as client:
453
+ return await client.tools.list(**params)
454
+
455
+
456
+ @mcp.tool()
457
+ async def tavus_tool_get(tool_id: str) -> Any:
458
+ async with TavusClient.from_env() as client:
459
+ return await client.tools.get(tool_id)
460
+
461
+
462
+ @mcp.tool()
463
+ async def tavus_tool_create(
464
+ name: str,
465
+ description: str,
466
+ parameters: dict[str, Any] | None = None,
467
+ delivery: dict[str, Any] | None = None,
468
+ origin: str = "llm",
469
+ on_call: str | None = None,
470
+ on_resolve: str | None = "fire_and_forget",
471
+ static_filler: str | None = None,
472
+ tags: list[str] | None = None,
473
+ ) -> Any:
474
+ """Create a tool.
475
+
476
+ `delivery` picks the channel: ``{"app_message": true}`` (Daily data
477
+ channel) or ``{"api": {"url": "https://...", "method": "POST", ...}}``
478
+ for direct third-party HTTPS calls. The HTTP form supports
479
+ ``headers``, ``query_params``, ``body_template``, ``content_type``,
480
+ ``timeout`` (default 10s, max 60), and ``auth`` with type ``none |
481
+ bearer | basic | api_key | hmac | oauth2_client_credentials``.
482
+ Placeholders ``{ident}`` in url/body/query must be declared in
483
+ ``parameters.properties``. Description + parameters JSON must total
484
+ ≤ 10,000 chars."""
485
+ body: dict[str, Any] = {
486
+ "name": name,
487
+ "description": description,
488
+ "parameters": parameters or {},
489
+ "delivery": delivery or {"app_message": True},
490
+ "origin": origin,
491
+ }
492
+ if on_call is not None:
493
+ body["on_call"] = on_call
494
+ if on_resolve is not None:
495
+ body["on_resolve"] = on_resolve
496
+ if static_filler is not None:
497
+ body["static_filler"] = static_filler
498
+ if tags is not None:
499
+ body["tags"] = tags
500
+ async with TavusClient.from_env() as client:
501
+ created = await client.tools.create(body)
502
+ # Surface same-origin saved tools so the agent reuses instead of
503
+ # duplicating. Injected as a namespaced key on the returned tool.
504
+ if isinstance(created, dict):
505
+ saved = _list_data(await client.tools.list(limit=100))
506
+ advisory = build_library_match_advisory(origin, name, saved)
507
+ if advisory:
508
+ created["_tool_reuse_advisory"] = advisory
509
+ return created
510
+
511
+
512
+ @mcp.tool()
513
+ async def tavus_tool_patch(
514
+ tool_id: str,
515
+ name: str | None = None,
516
+ description: str | None = None,
517
+ parameters: dict[str, Any] | None = None,
518
+ delivery: dict[str, Any] | None = None,
519
+ origin: str | None = None,
520
+ on_call: str | None = None,
521
+ on_resolve: str | None = None,
522
+ static_filler: str | None = None,
523
+ tags: list[str] | None = None,
524
+ ) -> Any:
525
+ """Update a tool. Omit fields to leave them unchanged. Secrets that came
526
+ back scrubbed (``********``) from a prior GET must be omitted, not
527
+ re-sent — RQH rejects the placeholder."""
528
+ body: dict[str, Any] = {}
529
+ if name is not None:
530
+ body["name"] = name
531
+ if description is not None:
532
+ body["description"] = description
533
+ if parameters is not None:
534
+ body["parameters"] = parameters
535
+ if delivery is not None:
536
+ body["delivery"] = delivery
537
+ if origin is not None:
538
+ body["origin"] = origin
539
+ if on_call is not None:
540
+ body["on_call"] = on_call
541
+ if on_resolve is not None:
542
+ body["on_resolve"] = on_resolve
543
+ if static_filler is not None:
544
+ body["static_filler"] = static_filler
545
+ if tags is not None:
546
+ body["tags"] = tags
547
+ async with TavusClient.from_env() as client:
548
+ return await client.tools.patch(tool_id, body)
549
+
550
+
551
+ @mcp.tool()
552
+ async def tavus_tool_delete(tool_id: str) -> Any:
553
+ async with TavusClient.from_env() as client:
554
+ return await client.tools.delete(tool_id)
555
+
556
+
557
+ @mcp.tool()
558
+ async def tavus_pronunciation_dictionary_list(
559
+ limit: int = 25,
560
+ page: int = 0,
561
+ sort: str = "desc",
562
+ ) -> Any:
563
+ """List pronunciation dictionaries — referenced from personas via
564
+ ``layers.tts.pronunciation_dictionary_id``."""
565
+ async with TavusClient.from_env() as client:
566
+ return await client.pronunciation_dictionaries.list(limit=limit, page=page, sort=sort)
567
+
568
+
569
+ @mcp.tool()
570
+ async def tavus_pronunciation_dictionary_get(dictionary_id: str) -> Any:
571
+ async with TavusClient.from_env() as client:
572
+ return await client.pronunciation_dictionaries.get(dictionary_id)
573
+
574
+
575
+ @mcp.tool()
576
+ async def tavus_pronunciation_dictionary_create(
577
+ name: str, rules: list[dict[str, Any]] | None = None
578
+ ) -> Any:
579
+ """Create a pronunciation dictionary. Each rule is
580
+ ``{text, pronunciation, type: "alias" | "ipa", alphabet?, case_sensitive?,
581
+ word_boundaries?}``. ``alias`` swaps the literal text for the
582
+ pronunciation string before TTS; ``ipa`` interprets the pronunciation
583
+ as IPA via SSML phoneme tags. ``text`` values must be unique within
584
+ the dictionary; max 10,000 rules per dictionary."""
585
+ body = {"name": name, "rules": rules or []}
586
+ async with TavusClient.from_env() as client:
587
+ return await client.pronunciation_dictionaries.create(body)
588
+
589
+
590
+ @mcp.tool()
591
+ async def tavus_pronunciation_dictionary_patch(
592
+ dictionary_id: str,
593
+ name: str | None = None,
594
+ rules: list[dict[str, Any]] | None = None,
595
+ ) -> Any:
596
+ """Patch a pronunciation dictionary. Supplying ``rules`` replaces the
597
+ full list (RQH does not merge rule arrays)."""
598
+ body: dict[str, Any] = {}
599
+ if name is not None:
600
+ body["name"] = name
601
+ if rules is not None:
602
+ body["rules"] = rules
603
+ async with TavusClient.from_env() as client:
604
+ return await client.pronunciation_dictionaries.patch(dictionary_id, body)
605
+
606
+
607
+ @mcp.tool()
608
+ async def tavus_pronunciation_dictionary_delete(dictionary_id: str) -> Any:
609
+ async with TavusClient.from_env() as client:
610
+ return await client.pronunciation_dictionaries.delete(dictionary_id)
611
+
612
+
613
+ @mcp.tool()
614
+ async def tavus_persona_tools_list(persona_id: str) -> Any:
615
+ """List the tools currently attached to a persona."""
616
+ async with TavusClient.from_env() as client:
617
+ return await client.personas.list_tools(persona_id)
618
+
619
+
620
+ @mcp.tool()
621
+ async def tavus_persona_tools_attach(persona_id: str, tool_ids: list[str]) -> Any:
622
+ """Attach one or more existing tools to a persona by ID. A persona can
623
+ hold up to 50 tools.
624
+
625
+ Vision/audio tools only fire when the persona runs Raven, so if any
626
+ attached tool is vision/audio this bumps the persona's perception_model to
627
+ raven-1 (response carries `_perception_model_bumped_to_raven_1`)."""
628
+ async with TavusClient.from_env() as client:
629
+ attached = await client.personas.attach_tools(persona_id, tool_ids)
630
+ tools_data = _list_data(attached)
631
+ origins = {t.get("origin") for t in tools_data if isinstance(t, dict)}
632
+ perception_bumped = False
633
+ if origins & {"vision", "audio"}:
634
+ persona = await client.personas.get(persona_id, include_settings="true")
635
+ layers = persona.get("layers") or {} if isinstance(persona, dict) else {}
636
+ perception = layers.get("perception")
637
+ if not isinstance(perception, dict):
638
+ op = {
639
+ "op": "add",
640
+ "path": "/layers/perception",
641
+ "value": {"perception_model": "raven-1"},
642
+ }
643
+ needs_bump = True
644
+ elif perception.get("perception_model") not in {"raven-0", "raven-1"}:
645
+ op = {
646
+ "op": "add",
647
+ "path": "/layers/perception/perception_model",
648
+ "value": "raven-1",
649
+ }
650
+ needs_bump = True
651
+ else:
652
+ needs_bump = False
653
+ if needs_bump:
654
+ await client.personas.patch(persona_id, [op])
655
+ perception_bumped = True
656
+ if isinstance(attached, dict):
657
+ attached["_perception_model_bumped_to_raven_1"] = perception_bumped
658
+ return attached
659
+
660
+
661
+ @mcp.tool()
662
+ async def tavus_persona_tools_detach(persona_id: str, tool_id: str) -> Any:
663
+ """Detach a tool from a persona."""
664
+ async with TavusClient.from_env() as client:
665
+ return await client.personas.detach_tool(persona_id, tool_id)
666
+
667
+
668
+ @mcp.tool()
669
+ async def tavus_builder_create(
670
+ name: str,
671
+ greeting: str | None = None,
672
+ persona_id: str | None = None,
673
+ model: str | None = None,
674
+ ) -> Any:
675
+ body: dict[str, Any] = {"name": name}
676
+ if greeting is not None:
677
+ body["greeting"] = greeting
678
+ if persona_id:
679
+ body["persona_id"] = persona_id
680
+ if model:
681
+ body["model"] = model
682
+ async with TavusClient.from_env() as client:
683
+ return await client.builders.create(body)
684
+
685
+
686
+ @mcp.tool()
687
+ async def tavus_builder_list(
688
+ limit: int | None = None,
689
+ page: int | None = None,
690
+ persona_id: str | None = None,
691
+ name: str | None = None,
692
+ status: str | None = None,
693
+ ) -> Any:
694
+ async with TavusClient.from_env() as client:
695
+ return await client.builders.list(
696
+ limit=limit, page=page, persona_id=persona_id, name=name, status=status
697
+ )
698
+
699
+
700
+ @mcp.tool()
701
+ async def tavus_builder_get(builder_id: str) -> Any:
702
+ async with TavusClient.from_env() as client:
703
+ return await client.builders.get(builder_id)
704
+
705
+
706
+ @mcp.tool()
707
+ async def tavus_builder_delete(builder_id: str) -> Any:
708
+ async with TavusClient.from_env() as client:
709
+ return await client.builders.delete(builder_id)
710
+
711
+
712
+ @mcp.tool()
713
+ async def tavus_builder_chat(builder_id: str, message: str) -> Any:
714
+ """Send a chat turn to the builder. Returns a structured response with
715
+ the assistant text, autocomplete suggestions, a `draft_ready` flag, and
716
+ the list of persona sections the builder LLM signaled as targets for
717
+ follow-up scoped updates (use ``tavus_builder_update_*`` to apply)."""
718
+ async with TavusClient.from_env() as client:
719
+ return await client.builders.chat(builder_id, message)
720
+
721
+
722
+ @mcp.tool()
723
+ async def tavus_builder_chat_history(builder_id: str, limit: int = 50) -> Any:
724
+ async with TavusClient.from_env() as client:
725
+ return await client.builders.chat_history(builder_id, limit=limit)
726
+
727
+
728
+ @mcp.tool()
729
+ async def tavus_builder_append_messages(builder_id: str, messages: list[dict[str, str]]) -> Any:
730
+ async with TavusClient.from_env() as client:
731
+ return await client.builders.append_messages(builder_id, messages)
732
+
733
+
734
+ @mcp.tool()
735
+ async def tavus_builder_update_objectives(builder_id: str, message: str) -> Any:
736
+ async with TavusClient.from_env() as client:
737
+ return await client.builders.update_objectives(builder_id, message)
738
+
739
+
740
+ @mcp.tool()
741
+ async def tavus_builder_update_guardrails(builder_id: str, message: str) -> Any:
742
+ async with TavusClient.from_env() as client:
743
+ return await client.builders.update_guardrails(builder_id, message)
744
+
745
+
746
+ @mcp.tool()
747
+ async def tavus_builder_update_greeting(builder_id: str, message: str) -> Any:
748
+ async with TavusClient.from_env() as client:
749
+ return await client.builders.update_greeting(builder_id, message)
750
+
751
+
752
+ @mcp.tool()
753
+ async def tavus_builder_update_personality(
754
+ builder_id: str,
755
+ message: str,
756
+ persona_name: bool = False,
757
+ system_prompt: bool = False,
758
+ ) -> Any:
759
+ async with TavusClient.from_env() as client:
760
+ return await client.builders.update_personality(
761
+ builder_id,
762
+ message,
763
+ persona_name=persona_name,
764
+ system_prompt=system_prompt,
765
+ )
766
+
767
+
768
+ @mcp.tool()
769
+ async def tavus_builder_publish(builder_id: str) -> Any:
770
+ async with TavusClient.from_env() as client:
771
+ return await client.builders.publish(builder_id)
772
+
773
+
774
+ @mcp.tool()
775
+ async def tavus_chat_start(
776
+ persona_id: str,
777
+ custom_greeting: str | None = None,
778
+ conversation_name: str | None = None,
779
+ ) -> Any:
780
+ """Start a text-only (chat-mode) conversation with a persona. Use this
781
+ to test a built persona by sending typed turns and reading replies; no
782
+ replica video is rendered."""
783
+ body: dict[str, Any] = {"persona_id": persona_id, "chat": True}
784
+ if custom_greeting:
785
+ body["custom_greeting"] = custom_greeting
786
+ if conversation_name:
787
+ body["conversation_name"] = conversation_name
788
+ async with TavusClient.from_env() as client:
789
+ return await client.conversations.create(body)
790
+
791
+
792
+ @mcp.tool()
793
+ async def tavus_chat_turn(
794
+ conversation_id: str,
795
+ text: str,
796
+ timeout_s: float = 20.0,
797
+ ) -> Any:
798
+ """Send one user turn to a chat-mode conversation and wait for the
799
+ persona's reply. Returns ``{text: <reply>}``."""
800
+ async with TavusClient.from_env() as client:
801
+ reply = await client.conversations.chat_turn(conversation_id, text, timeout_s=timeout_s)
802
+ return {"text": reply}
803
+
804
+
805
+ @mcp.tool()
806
+ async def tavus_chat_end(conversation_id: str) -> Any:
807
+ """End a chat-mode conversation. Same call as ``tavus_conversation_end``."""
808
+ async with TavusClient.from_env() as client:
809
+ return await client.conversations.end(conversation_id)
810
+
811
+
812
+ @mcp.tool()
813
+ async def tavus_persona_preview(
814
+ persona_id: str,
815
+ replica_id: str | None = None,
816
+ conversation_name: str | None = None,
817
+ ) -> Any:
818
+ """Start a full (audio/video) preview conversation against a persona and
819
+ return ``{conversation_id, conversation_url, ...}``. Hand the URL to a
820
+ human when you want them to visually verify the persona's behavior."""
821
+ body: dict[str, Any] = {"persona_id": persona_id}
822
+ if replica_id:
823
+ body["replica_id"] = replica_id
824
+ if conversation_name:
825
+ body["conversation_name"] = conversation_name
826
+ async with TavusClient.from_env() as client:
827
+ if not replica_id:
828
+ persona = await client.personas.get(persona_id)
829
+ default_replica = (
830
+ persona.get("default_replica_id") if isinstance(persona, dict) else None
831
+ )
832
+ if default_replica:
833
+ body["replica_id"] = default_replica
834
+ return await client.conversations.create(body)
835
+
836
+
837
+ @mcp.tool()
838
+ async def tavus_persona_build_and_verify(
839
+ prompt: str,
840
+ replica_id: str | None = None,
841
+ max_rounds: int = 4,
842
+ answers: list[str] | None = None,
843
+ ) -> Any:
844
+ """Build a persona from one creator prompt, optionally using supplied
845
+ creator answers for the builder's follow-up questions, then publish and
846
+ validate it through CVI chat mode. When ``replica_id`` is omitted, RQH
847
+ selects and attaches the default replica. Validation probes are generated
848
+ from the resulting persona spec."""
849
+ async with TavusClient.from_env() as client:
850
+ return await build_and_verify(
851
+ client,
852
+ prompt=prompt,
853
+ replica_id=replica_id,
854
+ max_rounds=max_rounds,
855
+ answers=answers,
856
+ auto_refine=True,
857
+ max_refine_rounds=1,
858
+ )
859
+
860
+
861
+ def _resource(client: TavusClient, resource: str) -> Any:
862
+ normalized = resource.strip().lower().replace("-", "_")
863
+ aliases = {
864
+ "guardrail": "guardrails",
865
+ "objective": "objectives",
866
+ "document": "documents",
867
+ "voice": "voices",
868
+ "tool": "tools",
869
+ }
870
+ attr = aliases.get(normalized, normalized)
871
+ if attr not in {"guardrails", "objectives", "documents", "voices", "tools"}:
872
+ raise ValueError("resource must be guardrails, objectives, documents, voices, or tools")
873
+ return getattr(client, attr)
874
+
875
+
876
+ def main() -> None:
877
+ mcp.run()