diff --git a/.github/workflows/publish-client-python.yml b/.github/workflows/publish-client-python.yml
index b3537b97..b6bd5504 100644
--- a/.github/workflows/publish-client-python.yml
+++ b/.github/workflows/publish-client-python.yml
@@ -1,4 +1,4 @@
-name: Publish PyPI Package
+name: Publish Client Python
on:
workflow_dispatch:
@@ -29,8 +29,10 @@ jobs:
# Build the package
- name: Build package
+ working-directory: sdks/python
run: uv build
# Publish to PyPI
- name: Publish to PyPI
+ working-directory: sdks/python
run: uv publish
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9c919b90..37b0c5af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.secret
+mcp.yaml
.gabber/
-.DS_Store
\ No newline at end of file
+.DS_Store
diff --git a/engine/pyproject.toml b/engine/pyproject.toml
index 35330bfe..e276bade 100644
--- a/engine/pyproject.toml
+++ b/engine/pyproject.toml
@@ -28,7 +28,7 @@ dependencies = [
"elevenlabs==1.12.1",
"pyvips~=2.0.2",
"av~=14.4.0",
- "openai~=1.93.0",
+ "openai~=1.107.1",
"pillow~=11.1.0",
"msgpack~=1.1.1",
"python-dotenv~=1.1.1",
diff --git a/engine/src/core/graph/runtime_api.py b/engine/src/core/graph/runtime_api.py
index 85f9a4c9..14784f83 100644
--- a/engine/src/core/graph/runtime_api.py
+++ b/engine/src/core/graph/runtime_api.py
@@ -11,7 +11,8 @@
from core.editor import serialize
from core.node import Node
from nodes.core.media.publish import Publish
-from nodes.core.tool import MCP
+
+PING_BYTES = "ping".encode("utf-8")
class RuntimeApi:
@@ -80,10 +81,14 @@ def on_pad(p: pad.Pad, value: Any):
p._add_update_handler(on_pad)
def on_data(packet: rtc.DataPacket):
- if not packet.topic or not packet.topic.startswith("runtime_api"):
+ if not packet.topic or packet.topic != "runtime_api":
return
- request = RuntimeRequest.model_validate_json(packet.data)
+ try:
+ request = RuntimeRequest.model_validate_json(packet.data)
+ except Exception as e:
+ logging.error(f"Invalid runtime_api request: {e}", exc_info=e)
+ return
req_id = request.req_id
ack_resp = RuntimeRequestAck(req_id=req_id, type="ack")
complete_resp = RuntimeResponse(req_id=req_id, type="complete")
diff --git a/engine/src/core/mcp/__init__.py b/engine/src/core/mcp/__init__.py
index 29ce78d8..247a6312 100644
--- a/engine/src/core/mcp/__init__.py
+++ b/engine/src/core/mcp/__init__.py
@@ -6,6 +6,7 @@
MCPServerConfig,
)
from .mcp_server_provider import MCPServerProvider
+from .datachannel_transport import datachannel_host
__all__ = [
"MCPTransport",
@@ -14,4 +15,5 @@
"MCPServer",
"MCPServerProvider",
"MCPServerConfig",
+ "datachannel_host",
]
diff --git a/engine/src/core/mcp/datachannel_transport.py b/engine/src/core/mcp/datachannel_transport.py
index ccf3385b..0084aaf6 100644
--- a/engine/src/core/mcp/datachannel_transport.py
+++ b/engine/src/core/mcp/datachannel_transport.py
@@ -19,7 +19,7 @@
@asynccontextmanager
async def datachannel_host(
- room: rtc.Room, participant: str
+ room: rtc.Room, participant: str, mcp_name: str
) -> AsyncGenerator[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
@@ -27,6 +27,7 @@ async def datachannel_host(
],
None,
]:
+ topic = f"__mcp__:{mcp_name}"
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
write_stream: MemoryObjectSendStream[SessionMessage]
@@ -38,7 +39,7 @@ async def datachannel_host(
packet_q = asyncio.Queue[rtc.DataPacket | None]()
def on_message(packet: rtc.DataPacket):
- if packet.topic != "__mcp__":
+ if packet.topic != topic:
return
if not packet.participant:
@@ -60,8 +61,12 @@ async def on_message_loop():
session_message = SessionMessage(message)
await read_stream_writer.send(session_message)
except ValidationError as exc:
+ logging.error(f"DC message validation error: {exc}")
# If JSON parse or model validation fails, send the exception
await read_stream_writer.send(exc)
+ except Exception as exc:
+ logging.error(f"DC unexpected error: {exc}")
+ await read_stream_writer.send(exc)
async def dc_writer():
"""
@@ -75,7 +80,7 @@ async def dc_writer():
by_alias=True, mode="json", exclude_none=True
)
await room.local_participant.publish_data(
- json.dumps(msg_dict), topic="__mcp__"
+ json.dumps(msg_dict), topic=topic
)
room.on("data_received", on_message)
@@ -91,42 +96,3 @@ async def dc_writer():
tg.cancel_scope.cancel()
room.off("data_received", on_message)
-
-
-async def datachannel_client_proxy(
- url: str,
- token: str,
- other_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
- other_write_stream: MemoryObjectSendStream[SessionMessage],
-) -> rtc.Room:
- """
- Connects to a LiveKit room and returns the Room object.
- """
- room = rtc.Room()
-
- def on_message(packet: rtc.DataPacket):
- if packet.topic != "__mcp__":
- return
- logger.debug(f"Received data packet: {packet.data}")
- json_msg = types.JSONRPCMessage.model_validate_json(packet.data)
- sm = SessionMessage(json_msg)
- other_write_stream.send_nowait(sm)
-
- async def read_loop():
- async with other_read_stream:
- async for session_message in other_read_stream:
- if isinstance(session_message, Exception):
- logger.error(f"Error in received message: {session_message}")
- continue
- msg_dict = session_message.message.model_dump(
- by_alias=True, mode="json", exclude_none=True
- )
- await room.local_participant.publish_data(
- json.dumps(msg_dict), topic="__mcp__"
- )
-
- room.on("data_received", on_message)
- await room.connect(url, token)
- await read_loop()
- room.off("data_received", on_message)
- return room
diff --git a/engine/src/core/mcp/mcp_server_config.py b/engine/src/core/mcp/mcp_server_config.py
index da45c27b..b4b55bfa 100644
--- a/engine/src/core/mcp/mcp_server_config.py
+++ b/engine/src/core/mcp/mcp_server_config.py
@@ -14,12 +14,26 @@ class MCPTransportSSE(BaseModel):
url: str
+class MCPTransportSTDIO(BaseModel):
+ type: Literal["stdio"] = "stdio"
+ command: str
+ args: list[str]
+
+
+MCPLocalTransport = Annotated[
+ MCPTransportSTDIO | MCPTransportSSE, Field(discriminator="type")
+]
+
+
class MCPTransportDatachannelProxy(BaseModel):
type: Literal["datachannel_proxy"] = "datachannel_proxy"
- local_transport: MCPTransportSSE
+ local_transport: MCPLocalTransport
-MCPTransport = Annotated[MCPTransportDatachannelProxy, Field(discriminator="type")]
+MCPTransport = Annotated[
+ MCPTransportDatachannelProxy | MCPTransportSTDIO | MCPTransportSTDIO,
+ Field(discriminator="type"),
+]
class MCPServer(BaseModel):
diff --git a/engine/src/core/pad/types.py b/engine/src/core/pad/types.py
index 38905a81..a7719b5b 100644
--- a/engine/src/core/pad/types.py
+++ b/engine/src/core/pad/types.py
@@ -19,6 +19,9 @@ def intersect(self, other: "BasePadType") -> "BasePadType | None":
return self
return None
+ def to_json_schema(self) -> dict[str, Any]:
+ raise NotImplementedError()
+
class String(BasePadType):
type: Literal["string"] = "string"
@@ -43,6 +46,14 @@ def intersect(self, other: "BasePadType"):
),
)
+ def to_json_schema(self) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "string"}
+ if self.max_length is not None:
+ schema["maxLength"] = self.max_length
+ if self.min_length is not None:
+ schema["minLength"] = self.min_length
+ return schema
+
class Enum(BasePadType):
type: Literal["enum"] = "enum"
@@ -75,6 +86,12 @@ def intersect(self, other: "BasePadType"):
raise ValueError("Unexpected state.")
+ def to_json_schema(self) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "string"}
+ if self.options is not None:
+ schema["enum"] = self.options
+ return schema
+
class Secret(BasePadType):
type: Literal["secret"] = "secret"
@@ -96,6 +113,9 @@ def intersect(self, other: "BasePadType"):
options=intersected_options,
)
+ def to_json_schema(self) -> dict[str, Any]:
+ raise NotImplementedError()
+
class Integer(BasePadType):
type: Literal["integer"] = "integer"
@@ -119,6 +139,14 @@ def intersect(self, other: "BasePadType"):
),
)
+ def to_json_schema(self) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "integer"}
+ if self.maximum is not None:
+ schema["maximum"] = self.maximum
+ if self.minimum is not None:
+ schema["minimum"] = self.minimum
+ return schema
+
class Float(BasePadType):
type: Literal["float"] = "float"
@@ -142,6 +170,14 @@ def intersect(self, other: "BasePadType"):
),
)
+ def to_json_schema(self) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "number"}
+ if self.maximum is not None:
+ schema["maximum"] = self.maximum
+ if self.minimum is not None:
+ schema["minimum"] = self.minimum
+ return schema
+
class BoundingBox(BasePadType):
type: Literal["bounding_box"] = "bounding_box"
@@ -154,6 +190,9 @@ class Point(BasePadType):
class Boolean(BasePadType):
type: Literal["boolean"] = "boolean"
+ def to_json_schema(self) -> dict[str, Any]:
+ return {"type": "boolean"}
+
class Audio(BasePadType):
type: Literal["audio"] = "audio"
@@ -211,6 +250,17 @@ def intersect(self, other: "BasePadType"):
),
)
+ def to_json_schema(self) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "array"}
+ if self.max_length is not None:
+ schema["maxItems"] = self.max_length
+ if (
+ self.item_type_constraints is not None
+ and len(self.item_type_constraints) == 1
+ ):
+ schema["items"] = self.item_type_constraints[0].to_json_schema()
+ return schema
+
class Schema(BasePadType):
type: Literal["schema"] = "schema"
@@ -243,6 +293,14 @@ def intersect(self, other: "BasePadType"):
return Object(object_schema=new_schema) if new_schema else None
+ def to_json_schema(self) -> dict[str, Any]:
+ if self.object_schema is not None:
+ return {
+ "type": "object",
+ "properties": self.object_schema,
+ }
+ return {"type": "object"}
+
class NodeReference(BasePadType):
type: Literal["node_reference"] = "node_reference"
diff --git a/engine/src/core/runtime_types/types.py b/engine/src/core/runtime_types/types.py
index c9638af1..bbc8c375 100644
--- a/engine/src/core/runtime_types/types.py
+++ b/engine/src/core/runtime_types/types.py
@@ -3,6 +3,7 @@
import asyncio
import base64
+import logging
from dataclasses import dataclass
from enum import Enum
from typing import Annotated, Any, Literal, cast
@@ -198,7 +199,7 @@ class ToolCall(BaseModel):
class ToolDefinition(BaseModel):
name: str
description: str
- parameters: "Schema | None" = None
+ parameters: "Schema | dict[str, Any] | None" = None
class ContextMessageContentItem_Audio(BaseModel):
@@ -275,15 +276,19 @@ class ContextMessageContent_ToolCallDelta(BaseModel):
class Schema(BaseModel):
properties: dict[
- str, pad.types.String | pad.types.Integer | pad.types.Float | pad.types.Boolean
+ str,
+ pad.types.String
+ | pad.types.Integer
+ | pad.types.Float
+ | pad.types.Boolean
+ | pad.types.Object
+ | pad.types.List,
]
required: list[str] | None = None
defaults: dict[str, Any] | None = None
def to_json_schema(self) -> dict[str, Any]:
- properties = {
- k: v.model_dump(exclude_none=True) for k, v in self.properties.items()
- }
+ properties = {k: v.to_json_schema() for k, v in self.properties.items()}
for d in self.defaults or {}:
if self.defaults is None:
continue
@@ -295,12 +300,54 @@ def to_json_schema(self) -> dict[str, Any]:
"required": self.required or [],
}
+ @classmethod
+ def from_json_schema(cls, schema: dict[str, Any]) -> "Schema":
+ properties: dict[
+ str,
+ pad.types.String
+ | pad.types.Integer
+ | pad.types.Float
+ | pad.types.Boolean
+ | pad.types.Object
+ | pad.types.List,
+ ] = {}
+ for key, value in schema.get("properties", {}).items():
+ type_ = value.get("type")
+ if type_ == "string":
+ properties[key] = pad.types.String()
+ elif type_ == "integer":
+ properties[key] = pad.types.Integer()
+ elif type_ == "number":
+ properties[key] = pad.types.Float()
+ elif type_ == "boolean":
+ properties[key] = pad.types.Boolean()
+ elif type_ == "object":
+ properties[key] = pad.types.Object()
+ elif type_ == "array":
+ properties[key] = pad.types.List(item_type_constraints=None)
+ else:
+ raise ValueError(f"Unsupported property type: {type_}")
+ return cls(
+ properties=properties,
+ required=schema.get("required", []),
+ defaults={
+ k: v.get("default")
+ for k, v in schema.get("properties", {}).items()
+ if "default" in v
+ },
+ )
+
def intersect(self, other: "Schema"):
if not isinstance(other, Schema):
return None
properties: dict[
str,
- pad.types.String | pad.types.Integer | pad.types.Float | pad.types.Boolean,
+ pad.types.String
+ | pad.types.Integer
+ | pad.types.Float
+ | pad.types.Boolean
+ | pad.types.Object
+ | pad.types.List,
] = {}
defaults: dict[str, Any] = {}
for d in self.defaults or {}:
diff --git a/engine/src/lib/llm/llm.py b/engine/src/lib/llm/llm.py
index fb1560b3..8a72b615 100644
--- a/engine/src/lib/llm/llm.py
+++ b/engine/src/lib/llm/llm.py
@@ -23,6 +23,7 @@
ContextMessageContentItem_Video,
ContextMessageRole,
ToolDefinition,
+ Schema,
)
from lib.video.mp4_encoder import MP4_Encoder
@@ -182,8 +183,16 @@ def to_openai_completion_tools_input(self) -> list[chat.ChatCompletionToolParam]
"properties": {},
"required": [],
}
- else:
+ elif isinstance(tool.parameters, dict):
+ parameters = tool.parameters
+ elif isinstance(tool.parameters, Schema):
parameters = tool.parameters.to_json_schema()
+ else:
+ parameters = {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ }
tools.append(
{
diff --git a/engine/src/main.py b/engine/src/main.py
index 902b8327..025b05f3 100644
--- a/engine/src/main.py
+++ b/engine/src/main.py
@@ -6,7 +6,6 @@
import logging
import os
-import aiohttp
import click
from pydantic import TypeAdapter
@@ -149,19 +148,5 @@ def generate_statemachine_schema():
print(json.dumps(config_schema, indent=2))
-@main_cli.command("mcp-proxy-client")
-def mcp_proxy():
- async def run_proxy_client():
- while True:
- try:
- pass
- except Exception as e:
- logging.error(f"Error in MCP proxy client: {e}", exc_info=True)
- await asyncio.sleep(5)
- continue
-
- asyncio.run(run_proxy_client())
-
-
if __name__ == "__main__":
main_cli()
diff --git a/engine/src/nodes/core/tool/mcp.py b/engine/src/nodes/core/tool/mcp.py
index dc37de93..3c1b4eec 100644
--- a/engine/src/nodes/core/tool/mcp.py
+++ b/engine/src/nodes/core/tool/mcp.py
@@ -3,10 +3,13 @@
import asyncio
import logging
-from typing import Any, cast
+from typing import Tuple, cast
+import contextlib
-from core import node, pad
+from core import node, pad, mcp, runtime_types
from core.node import NodeMetadata
+from mcp.types import ContentBlock
+from mcp import ClientSession
class MCP(node.Node):
@@ -44,7 +47,96 @@ def resolve_pads(self):
self.pads = [self_pad, mcp_server]
async def run(self):
- pass
+ self.init_lock = asyncio.Lock()
+ self.session: ClientSession | None = None
+ while True:
+ exit_stack = contextlib.AsyncExitStack()
+ async with self.init_lock:
+ self.session = await self.create_session(exit_stack)
+ try:
+ await asyncio.wait_for(self.session.initialize(), timeout=2)
+ await asyncio.sleep(1)
+ except asyncio.TimeoutError:
+ logging.error("MCP Client session timeout")
+ await exit_stack.aclose()
+ continue
- async def generate_mcp_server(self):
- pass
+ try:
+ await self.session_ping_loop(self.session)
+ except Exception as e:
+ logging.error(f"MCP Client list_tools error: {e}")
+ await exit_stack.aclose()
+ continue
+
+ async def create_session(self, exit_stack: contextlib.AsyncExitStack):
+ mcp_server_pad = cast(pad.PropertySinkPad, self.get_pad("mcp_server"))
+ if not mcp_server_pad or not mcp_server_pad.get_value():
+ raise ValueError("MCP server pad not configured")
+
+ mcp_server_name = mcp_server_pad.get_value()
+ mcp_server = next(
+ (s for s in self.mcp_servers if s.name == mcp_server_name), None
+ )
+ if not mcp_server:
+ raise ValueError(f"MCP server '{mcp_server_name}' not found")
+
+ if not isinstance(mcp_server.transport, mcp.MCPTransportDatachannelProxy):
+ raise ValueError(
+ "Only MCPTransportDatachannelProxy is supported in this node"
+ )
+
+ logging.info(f"Connecting to MCP server '{mcp_server.name}'")
+ read_stream, write_stream = await exit_stack.enter_async_context(
+ mcp.datachannel_host(self.room, "mcp_proxy", mcp_server.name)
+ )
+ session = await exit_stack.enter_async_context(
+ ClientSession(read_stream=read_stream, write_stream=write_stream)
+ )
+ return session
+
+ async def session_ping_loop(self, session: ClientSession):
+ try:
+ while True:
+ await asyncio.sleep(10)
+ await asyncio.wait_for(session.send_ping(), timeout=2)
+ except asyncio.CancelledError:
+ logging.info("MCP Client ping loop cancelled")
+ raise
+
+ async def to_tool_definitions(self) -> list[runtime_types.ToolDefinition]:
+ async with self.init_lock:
+ if not self.session:
+ raise ValueError("MCP session not initialized")
+ mcp_tools_res = await self.session.list_tools()
+ mcp_tools = mcp_tools_res.tools
+ tool_defs: list[runtime_types.ToolDefinition] = []
+ for t in mcp_tools:
+ tool_def = runtime_types.ToolDefinition(
+ name=t.name,
+ description=t.description or "",
+ parameters=t.inputSchema,
+ )
+ tool_defs.append(tool_def)
+
+ return tool_defs
+
+ async def call_tool(self, tool_call: runtime_types.ToolCall):
+ logging.info(f"MCP Client calling tool '{tool_call.name}'")
+ sess: ClientSession
+ async with self.init_lock:
+ if not self.session:
+ raise ValueError("MCP session not initialized")
+ sess = self.session
+
+ results: list[ContentBlock] | Exception = []
+ try:
+ response = await asyncio.wait_for(
+ sess.call_tool(tool_call.name, tool_call.arguments), timeout=15
+ )
+ results = response.content if response.content else []
+ except asyncio.TimeoutError:
+ results = Exception(f"Tool call '{tool_call.name}' timed out.")
+ except Exception as e:
+ results = Exception(f"Error calling tool '{tool_call.name}': {e}")
+
+ return results
diff --git a/engine/src/nodes/core/tool/tool_group.py b/engine/src/nodes/core/tool/tool_group.py
index 3de2af76..37a2e57c 100644
--- a/engine/src/nodes/core/tool/tool_group.py
+++ b/engine/src/nodes/core/tool/tool_group.py
@@ -83,6 +83,12 @@ def resolve_pads(self):
self.pads = [num_tools, self_pad] + tools
+ def has_tool(self, tool_name: str) -> bool:
+ for tool in self.tool_nodes:
+ if tool.get_name() == tool_name:
+ return True
+ return False
+
async def call_tools(
self,
tool_calls: list[runtime_types.ToolCall],
diff --git a/engine/src/nodes/llm/base_llm.py b/engine/src/nodes/llm/base_llm.py
index ef80c128..34e9fe84 100644
--- a/engine/src/nodes/llm/base_llm.py
+++ b/engine/src/nodes/llm/base_llm.py
@@ -4,12 +4,21 @@
import asyncio
import logging
from abc import ABC, abstractmethod
-from typing import cast
+from typing import cast, Tuple
from core import node, pad, runtime_types
+from nodes.core.tool import mcp
from lib.llm import AsyncLLMResponseHandle, LLMRequest, openai_compatible
from utils import get_full_content_from_deltas, get_tool_calls_from_choice_deltas
from nodes.core.tool import Tool, ToolGroup
+from mcp.types import (
+ ContentBlock,
+ TextContent,
+ ImageContent,
+ AudioContent,
+ ResourceLink,
+ EmbeddedResource,
+)
class BaseLLM(node.Node, ABC):
@@ -25,7 +34,7 @@ def model(self) -> str: ...
@abstractmethod
async def api_key(self) -> str: ...
- def resolve_pads(self):
+ def get_base_pads(self):
run_trigger = cast(pad.StatelessSinkPad, self.get_pad("run_trigger"))
if not run_trigger:
run_trigger = pad.StatelessSinkPad(
@@ -34,7 +43,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(run_trigger)
started_source = cast(pad.StatelessSourcePad, self.get_pad("started"))
if not started_source:
@@ -44,7 +52,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(started_source)
tool_calls_started_source = cast(
pad.StatelessSourcePad, self.get_pad("tool_calls_started")
@@ -56,7 +63,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(tool_calls_started_source)
tool_calls_finished_source = cast(
pad.StatelessSourcePad, self.get_pad("tool_calls_finished")
@@ -68,7 +74,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(tool_calls_finished_source)
first_token_source = cast(pad.StatelessSourcePad, self.get_pad("first_token"))
if not first_token_source:
@@ -78,7 +83,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(first_token_source)
text_stream_source = cast(pad.StatelessSourcePad, self.get_pad("text_stream"))
if not text_stream_source:
@@ -88,7 +92,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.TextStream()],
)
- self.pads.append(text_stream_source)
thinking_stream_source = cast(
pad.StatelessSourcePad, self.get_pad("thinking_stream")
@@ -100,7 +103,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.TextStream()],
)
- self.pads.append(thinking_stream_source)
context_message_source = cast(
pad.StatelessSourcePad, self.get_pad("context_message")
@@ -112,7 +114,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.ContextMessage()],
)
- self.pads.append(context_message_source)
finished_source = cast(pad.StatelessSourcePad, self.get_pad("finished"))
if not finished_source:
@@ -122,7 +123,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(finished_source)
cancel_trigger = cast(pad.StatelessSinkPad, self.get_pad("cancel_trigger"))
if not cancel_trigger:
@@ -132,7 +132,6 @@ def resolve_pads(self):
owner_node=self,
default_type_constraints=[pad.types.Trigger()],
)
- self.pads.append(cancel_trigger)
context_sink = cast(pad.PropertySinkPad, self.get_pad("context"))
if not context_sink:
@@ -155,10 +154,10 @@ def resolve_pads(self):
)
],
)
- self.pads.append(context_sink)
+ tool_group_sink = cast(pad.PropertySinkPad, self.get_pad("tool_group"))
+ mcp_pads: list[pad.PropertySinkPad] = []
if self.supports_tool_calls():
- tool_group_sink = cast(pad.PropertySinkPad, self.get_pad("tool_group"))
if not tool_group_sink:
tool_group_sink = pad.PropertySinkPad(
id="tool_group",
@@ -169,7 +168,61 @@ def resolve_pads(self):
],
value=None,
)
- self.pads.append(tool_group_sink)
+
+ mcp_pads = self.mcp_server_pads()
+
+ base_sink_pads: list[pad.SinkPad] = [
+ run_trigger,
+ cancel_trigger,
+ context_sink,
+ ]
+ base_source_pads: list[pad.SourcePad] = [
+ started_source,
+ first_token_source,
+ text_stream_source,
+ thinking_stream_source,
+ context_message_source,
+ finished_source,
+ ]
+
+ if tool_group_sink is not None:
+ base_sink_pads.append(tool_group_sink)
+ base_sink_pads += mcp_pads
+ base_source_pads.append(tool_calls_started_source)
+ base_source_pads.append(tool_calls_finished_source)
+
+ return base_sink_pads, base_source_pads
+
+ def mcp_server_pads(self):
+ exising_mcp_pads = [
+ p
+ for p in self.pads
+ if isinstance(p, pad.PropertySinkPad)
+ and p.get_id().startswith("mcp_server_")
+ ]
+
+ connected_mcp_pads = [
+ p for p in exising_mcp_pads if p.get_previous_pad() is not None
+ ]
+
+ renamed_connected_mcp_pads = []
+ for i, cp in enumerate(connected_mcp_pads):
+ new_id = f"mcp_server_{i}"
+ if cp.get_id() != new_id:
+ cp.set_id(new_id)
+ renamed_connected_mcp_pads.append(cp)
+
+ empty_pad = pad.PropertySinkPad(
+ id=f"mcp_server_{len(renamed_connected_mcp_pads)}",
+ group="mcp_server",
+ owner_node=self,
+ default_type_constraints=[
+ pad.types.NodeReference(node_types=["MCP"]),
+ ],
+ value="None",
+ )
+
+ return renamed_connected_mcp_pads + [empty_pad]
async def run(self):
cancel_trigger = cast(
@@ -185,12 +238,6 @@ async def run(self):
finished_source = cast(
pad.StatelessSourcePad, self.get_pad_required("finished")
)
- tool_calls_started_source = cast(
- pad.StatelessSourcePad, self.get_pad_required("tool_calls_started")
- )
- tool_calls_finished_source = cast(
- pad.StatelessSourcePad, self.get_pad_required("tool_calls_finished")
- )
context_sink = cast(pad.PropertySinkPad, self.get_pad_required("context"))
context_message_source = cast(
pad.StatelessSourcePad, self.get_pad_required("context_message")
@@ -245,9 +292,12 @@ async def cancel_task():
item.ctx.complete()
async def generation_task(
- handle: AsyncLLMResponseHandle, ctx: pad.RequestContext
+ handle: AsyncLLMResponseHandle,
+ ctx: pad.RequestContext,
+ tg_tools: list[runtime_types.ToolDefinition],
+ mcp_tools: dict[mcp.MCP, list[runtime_types.ToolDefinition]],
):
- tool_task: asyncio.Task[list[str]] | None = None
+ tool_task: asyncio.Task[list[runtime_types.ContextMessage]] | None = None
all_deltas: list[runtime_types.ContextMessageContent_ChoiceDelta] = []
text_stream = runtime_types.TextStream()
thinking_stream = runtime_types.TextStream()
@@ -288,14 +338,14 @@ async def generation_task(
all_tool_calls: list[runtime_types.ToolCall] = []
if tool_group_sink is not None:
all_tool_calls = get_tool_calls_from_choice_deltas(all_deltas)
- if len(all_tool_calls) > 0:
- tool_calls_started_source.push_item(
- runtime_types.Trigger(), ctx
- )
- tg = cast(ToolGroup, tool_group_sink.get_value())
- tool_task = asyncio.create_task(
- tg.call_tools(all_tool_calls, ctx)
+ tool_task = asyncio.create_task(
+ self.call_tools(
+ all_tool_calls=all_tool_calls,
+ tg_tool_defns=tg_tool_definitions,
+ mcp_tool_defns=mcp_tools,
+ ctx=ctx,
)
+ )
full_content = get_full_content_from_deltas(all_deltas)
context_message_source.push_item(
@@ -312,33 +362,13 @@ async def generation_task(
)
if tool_task is not None:
- tool_results = await tool_task
- for i in range(len(all_tool_calls)):
- context_message_source.push_item(
- runtime_types.ContextMessage(
- role=runtime_types.ContextMessageRole.TOOL,
- content=[
- runtime_types.ContextMessageContentItem_Text(
- content=tool_results[i]
- )
- ],
- tool_call_id=all_tool_calls[i].call_id,
- tool_calls=[],
- ),
- ctx,
- )
- tool_calls_finished_source.push_item(runtime_types.Trigger(), ctx)
- # TODO look at the finished/ctx.complete() logic here
+ tool_msgs = await tool_task
+ for msg in tool_msgs:
+ context_message_source.push_item(msg, ctx)
except asyncio.CancelledError:
- finished_source.push_item(runtime_types.Trigger(), ctx)
- ctx.complete()
+ pass
except Exception as e:
logging.error(f"Error during LLM generation: {e}", exc_info=e)
- if tool_task is not None:
- tool_calls_finished_source.push_item(runtime_types.Trigger(), ctx)
-
- finished_source.push_item(runtime_types.Trigger(), ctx)
- ctx.complete()
finally:
finished_source.push_item(runtime_types.Trigger(), ctx)
ctx.complete()
@@ -355,16 +385,29 @@ def done_callback(task: asyncio.Task):
async for item in run_trigger:
ctx = item.ctx
messages = context_sink.get_value()
- tool_definitions: list[runtime_types.ToolDefinition] = []
+ all_tool_definitions: list[runtime_types.ToolDefinition] = []
+ tg_tool_definitions: list[runtime_types.ToolDefinition] = []
if tool_group_sink is not None and tool_group_sink.get_value() is not None:
tool_nodes = cast(ToolGroup, tool_group_sink.get_value()).tool_nodes
for tn in tool_nodes:
- if not isinstance(tn, Tool):
- logging.warning(f"Node {tn.id} is not a Tool, skipping.")
- continue
td = tn.get_tool_definition()
- tool_definitions.append(td)
- request = LLMRequest(context=messages, tool_definitions=tool_definitions)
+ tg_tool_definitions.append(td)
+ all_tool_definitions.append(td)
+ mcp_tool_definitions: dict[mcp.MCP, list[runtime_types.ToolDefinition]] = {}
+ mcp_sinks = self.mcp_server_pads()
+ for mcp_sink in mcp_sinks:
+ mcp_node = cast(mcp.MCP, mcp_sink.get_value())
+ if not isinstance(mcp_node, mcp.MCP):
+ continue
+ if mcp_node not in mcp_tool_definitions:
+ mcp_tool_definitions[mcp_node] = []
+ tdfs = await mcp_node.to_tool_definitions()
+ mcp_tool_definitions[mcp_node].extend(tdfs)
+ all_tool_definitions.extend(tdfs)
+
+ request = LLMRequest(
+ context=messages, tool_definitions=all_tool_definitions
+ )
if running_handle is not None:
logging.warning(
"LLM is already running a generation, skipping new request."
@@ -378,7 +421,14 @@ def done_callback(task: asyncio.Task):
video_support=video_supported,
audio_support=audio_supported,
)
- t = asyncio.create_task(generation_task(running_handle, ctx))
+ t = asyncio.create_task(
+ generation_task(
+ running_handle,
+ ctx,
+ tg_tools=tg_tool_definitions,
+ mcp_tools=mcp_tool_definitions,
+ )
+ )
tasks.add(t)
t.add_done_callback(done_callback)
except Exception as e:
@@ -386,6 +436,85 @@ def done_callback(task: asyncio.Task):
finished_source.push_item(runtime_types.Trigger(), ctx)
await cancel_task_t
+ async def call_tools(
+ self,
+ *,
+ tg_tool_defns: list[runtime_types.ToolDefinition],
+ mcp_tool_defns: dict[mcp.MCP, list[runtime_types.ToolDefinition]],
+ all_tool_calls: list[runtime_types.ToolCall],
+ ctx: pad.RequestContext,
+ ) -> list[runtime_types.ContextMessage]:
+ tg_tool_calls = [t for t in all_tool_calls if t.name in tg_tool_defns]
+
+ results: list[runtime_types.ContextMessage] = []
+
+ all_tasks: list[asyncio.Task] = []
+
+ async def run_tg_task():
+ tg_res = await self.call_tg_calls(tg_tool_calls=tg_tool_calls, ctx=ctx)
+ for i, res in enumerate(tg_res):
+ msg = runtime_types.ContextMessage(
+ role=runtime_types.ContextMessageRole.TOOL,
+ content=[runtime_types.ContextMessageContentItem_Text(content=res)],
+ tool_call_id=tg_tool_calls[i].call_id,
+ tool_calls=[],
+ )
+ results.append(msg)
+
+ async def run_mcp_task(node: mcp.MCP, tc: runtime_types.ToolCall):
+ res = await node.call_tool(tc)
+ if isinstance(res, Exception):
+ logging.error(f"Error calling MCP tool '{tc.name}': {res}")
+ msg = runtime_types.ContextMessage(
+ role=runtime_types.ContextMessageRole.TOOL,
+ content=[
+ runtime_types.ContextMessageContentItem_Text(
+ content=f"Error calling tool '{tc.name}': {res}"
+ )
+ ],
+ tool_call_id=tc.call_id,
+ tool_calls=[],
+ )
+ else:
+ contents: list[runtime_types.ContextMessageContentItem] = []
+ for block in res:
+ if isinstance(block, TextContent):
+ content = runtime_types.ContextMessageContentItem_Text(
+ content=block.text
+ )
+ contents.append(content)
+ else:
+ logging.error(f"Error calling MCP tool '{tc.name}': {res}")
+ msg = runtime_types.ContextMessage(
+ role=runtime_types.ContextMessageRole.TOOL,
+ content=contents,
+ tool_call_id=tc.call_id,
+ tool_calls=[],
+ )
+ results.append(msg)
+
+ if len(tg_tool_calls) > 0:
+ tg_task = asyncio.create_task(run_tg_task())
+ all_tasks.append(tg_task)
+
+ for mcp_node in mcp_tool_defns.keys():
+ mcp_defns = mcp_tool_defns[mcp_node]
+ mcp_defn_names = [td.name for td in mcp_defns]
+ mcp_calls = [tc for tc in all_tool_calls if tc.name in mcp_defn_names]
+ for tc in mcp_calls:
+ mcp_t = asyncio.create_task(run_mcp_task(mcp_node, tc))
+ all_tasks.append(mcp_t)
+
+ await asyncio.gather(*all_tasks)
+ return results
+
+ async def call_tg_calls(self, tg_tool_calls: list[runtime_types.ToolCall], ctx):
+ tool_group_sink = cast(pad.PropertySinkPad, self.get_pad_required("tool_group"))
+ if tool_group_sink is None or tool_group_sink.get_value() is None:
+ raise ValueError("Tool group is not configured for tool calls.")
+ tg = cast(ToolGroup, tool_group_sink.get_value())
+ return await tg.call_tools(tg_tool_calls, ctx)
+
async def _supports_video(self, llm: openai_compatible.OpenAICompatibleLLM) -> bool:
dummy_request = LLMRequest(
context=[
diff --git a/engine/src/nodes/llm/openai_compatible_llm.py b/engine/src/nodes/llm/openai_compatible_llm.py
index b2298dfa..789eb6d4 100644
--- a/engine/src/nodes/llm/openai_compatible_llm.py
+++ b/engine/src/nodes/llm/openai_compatible_llm.py
@@ -1,6 +1,7 @@
# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
# SPDX-License-Identifier: SUL-1.0
+import logging
from typing import cast
from core import pad
@@ -47,7 +48,7 @@ async def api_key(self) -> str:
return await self.secret_provider.resolve_secret(api_key_name)
def resolve_pads(self):
- super().resolve_pads()
+ base_sink_pads, base_source_pads = self.get_base_pads()
base_url_sink = cast(pad.PropertySinkPad, self.get_pad("base_url"))
if not base_url_sink:
base_url_sink = pad.PropertySinkPad(
@@ -57,7 +58,6 @@ def resolve_pads(self):
default_type_constraints=[pad.types.String()],
value="https://api.openai.com/v1",
)
- self.pads.append(base_url_sink)
api_key_sink = cast(pad.PropertySinkPad, self.get_pad("api_key"))
if not api_key_sink:
@@ -68,7 +68,6 @@ def resolve_pads(self):
default_type_constraints=[pad.types.Secret(options=[])],
value="",
)
- self.pads.append(api_key_sink)
model_sink = cast(pad.PropertySinkPad, self.get_pad("model"))
if not model_sink:
@@ -79,8 +78,11 @@ def resolve_pads(self):
default_type_constraints=[pad.types.String()],
value="gpt-4.1-mini",
)
- self.pads.append(model_sink)
+
+ base_sink_pads.extend([base_url_sink, api_key_sink, model_sink])
cast(list[pad.types.Secret], api_key_sink.get_type_constraints())[
0
].options = self.secrets
+
+ self.pads = cast(list[pad.Pad], base_sink_pads + base_source_pads)
diff --git a/engine/src/services/engine.py b/engine/src/services/engine.py
index a38a73c8..2c47f932 100644
--- a/engine/src/services/engine.py
+++ b/engine/src/services/engine.py
@@ -58,9 +58,7 @@ def cpu_load_fnc(worker: agents.Worker) -> float:
async def req_fnc(worker: agents.JobRequest):
- logging.info("NEIL Requesting job for gabber-engine")
await worker.accept(name="gabber-engine", identity="gabber-engine")
- logging.info("NEIL Requesting job for gabber-engine 2")
def run_engine(
diff --git a/engine/src/services/mcp_proxy_client.py b/engine/src/services/mcp_proxy_client.py
deleted file mode 100644
index 4e3ed656..00000000
--- a/engine/src/services/mcp_proxy_client.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
-# SPDX-License-Identifier: SUL-1.0
-
-
-class MCPProxyClientService:
- def __init__(self):
- pass
-
- async def run(self):
- pass
-
-
-class MCPProxyClient:
- def __init__(self, *, connection_token: str):
- pass
-
- async def run(self):
- pass
diff --git a/engine/src/services/repository/repository.py b/engine/src/services/repository/repository.py
index bf07165c..93123e21 100644
--- a/engine/src/services/repository/repository.py
+++ b/engine/src/services/repository/repository.py
@@ -758,7 +758,7 @@ async def mcp_proxy_connection(self, request: aiohttp.web.Request):
can_publish_data=True,
can_subscribe=True,
)
- ).with_identity("debug")
+ ).with_identity("mcp_proxy")
connection_details = models.AppRunConnectionDetails(
url=livekit_url,
diff --git a/engine/uv.lock b/engine/uv.lock
index 436c323a..3aac3377 100644
--- a/engine/uv.lock
+++ b/engine/uv.lock
@@ -376,7 +376,7 @@ requires-dist = [
{ name = "nltk" },
{ name = "numpy" },
{ name = "onnxruntime", specifier = ">=1.17.1" },
- { name = "openai", specifier = "~=1.93.0" },
+ { name = "openai", specifier = "~=1.107.1" },
{ name = "opencv-python-headless", specifier = "~=4.12.0" },
{ name = "pillow", specifier = "~=11.1.0" },
{ name = "posthog" },
@@ -789,7 +789,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.93.3"
+version = "1.107.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -801,9 +801,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e0/66/fadc0cad6a229c6a85c3aa5f222a786ec4d9bf14c2a004f80ffa21dbaf21/openai-1.93.3.tar.gz", hash = "sha256:488b76399238c694af7e4e30c58170ea55e6f65038ab27dbe95b5077a00f8af8", size = 487595, upload_time = "2025-07-09T14:08:27.789Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/e0/a62daa7ff769df969cc1b782852cace79615039630b297005356f5fb46fb/openai-1.107.1.tar.gz", hash = "sha256:7c51b6b8adadfcf5cada08a613423575258b180af5ad4bc2954b36ebc0d3ad48", size = 563671, upload_time = "2025-09-10T15:04:40.288Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/b9/0df6351b25c6bd494c534d2a8191dc9460fb5bb09c88b1427775d49fde05/openai-1.93.3-py3-none-any.whl", hash = "sha256:41aaa7594c7d141b46eed0a58dcd75d20edcc809fdd2c931ecbb4957dc98a892", size = 755132, upload_time = "2025-07-09T14:08:25.533Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/12/32c19999a58eec4a695e8ce334442b6135df949f0bb61b2ceaa4fa60d3a9/openai-1.107.1-py3-none-any.whl", hash = "sha256:168f9885b1b70d13ada0868a0d0adfd538c16a02f7fd9fe063851a2c9a025e72", size = 945177, upload_time = "2025-09-10T15:04:37.782Z" },
]
[[package]]
diff --git a/frontend/app/app/[app]/client_page.tsx b/frontend/app/app/[app]/client_page.tsx
index c9af4c57..d6c5fa50 100644
--- a/frontend/app/app/[app]/client_page.tsx
+++ b/frontend/app/app/[app]/client_page.tsx
@@ -38,8 +38,7 @@ export function ClientPage({ existingApp }: Props) {
const startRunImpl = useCallback(
async (params: { graph: GraphEditorRepresentation }) => {
- const resp = await createAppRun({ graph: params.graph });
- return resp.connection_details;
+ return await createAppRun({ graph: params.graph });
},
[],
);
diff --git a/frontend/app/debug/[run_id]/client_page.tsx b/frontend/app/debug/[run_id]/client_page.tsx
index 137bb130..06319b2e 100644
--- a/frontend/app/debug/[run_id]/client_page.tsx
+++ b/frontend/app/debug/[run_id]/client_page.tsx
@@ -14,16 +14,17 @@ import { DebugGraph } from "@/components/debug/DebugGraph";
type Props = {
connectionDetails: AppRunConnectionDetails;
+ runId: string;
graph: GraphEditorRepresentation;
};
-export function ClientPage({ graph, connectionDetails }: Props) {
+export function ClientPage({ graph, connectionDetails, runId }: Props) {
const saveImpl = useCallback(async () => {
throw new Error("saveImpl is not implemented for Debug mode");
}, []);
const startRunImpl = useCallback(async () => {
- return connectionDetails;
- }, [connectionDetails]);
+ return { connectionDetails, runId };
+ }, [connectionDetails, runId]);
return (
diff --git a/frontend/app/example/[example]/client_page.tsx b/frontend/app/example/[example]/client_page.tsx
index c22883bc..789d6e29 100644
--- a/frontend/app/example/[example]/client_page.tsx
+++ b/frontend/app/example/[example]/client_page.tsx
@@ -25,9 +25,7 @@ export function ClientPage({ existingExample: existingApp }: Props) {
const startRunImpl = useCallback(
async (params: { graph: GraphEditorRepresentation }) => {
- const resp = await createAppRun({ graph: params.graph });
- const connDetails = resp.connection_details;
- return connDetails;
+ return await createAppRun({ graph: params.graph });
},
[],
);
@@ -39,6 +37,7 @@ export function ClientPage({ existingExample: existingApp }: Props) {
editor_url="ws://localhost:8000/ws"
savedGraph={existingApp.graph}
saveImpl={saveImpl}
+ debug={false}
>
diff --git a/frontend/components/RunId.tsx b/frontend/components/RunId.tsx
new file mode 100644
index 00000000..0b27f663
--- /dev/null
+++ b/frontend/components/RunId.tsx
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
+ * SPDX-License-Identifier: SUL-1.0
+ */
+
+import { useRun } from "@/hooks/useRun";
+import toast from "react-hot-toast";
+
+export function RunId() {
+ const { runId } = useRun();
+
+ if (!runId) {
+ return null;
+ }
+
+ const handleCopy = async () => {
+ if (navigator.clipboard) {
+ toast.success("Run ID copied to clipboard");
+ await navigator.clipboard.writeText(runId);
+ }
+ };
+
+ return (
+
+
run_id:
+
+
+ );
+}
diff --git a/frontend/components/flow/FlowEdit.tsx b/frontend/components/flow/FlowEdit.tsx
index 9cb7ae2a..b4da662f 100644
--- a/frontend/components/flow/FlowEdit.tsx
+++ b/frontend/components/flow/FlowEdit.tsx
@@ -38,6 +38,7 @@ import {
PadEditorRepresentation,
PortalEnd,
} from "@/generated/editor";
+import { RunId } from "../RunId";
const edgeTypes = {
hybrid: HybridEdge,
@@ -146,6 +147,11 @@ function FlowEditInner({ editable }: Props) {
{connectionStatus}
)}
+ {
+
+
+
+ }
void;
startRun: (params: { graph: GraphEditorRepresentation }) => void;
};
const RunContext = createContext(undefined);
+type GenerateConnectionDetails = (params: {
+ graph: GraphEditorRepresentation;
+}) => Promise<{ connectionDetails: ConnectionDetails; runId: string }>;
+
interface RunProviderProps {
children: React.ReactNode;
- generateConnectionDetailsImpl: (params: {
- graph: GraphEditorRepresentation;
- }) => Promise;
+ generateConnectionDetailsImpl: GenerateConnectionDetails;
}
export function RunProvider({
@@ -55,11 +58,10 @@ function Inner({
children,
}: {
children?: React.ReactNode;
- generateConnectionDetailsImpl: (params: {
- graph: GraphEditorRepresentation;
- }) => Promise;
+ generateConnectionDetailsImpl: GenerateConnectionDetails;
}) {
const { connect, disconnect, connectionState } = useEngine();
+ const [runId, setRunId] = useState(null);
const [starting, setStarting] = useState(false);
const startingRef = useRef(false);
@@ -75,7 +77,8 @@ function Inner({
const res = await generateConnectionDetailsImpl({
graph: params.graph,
});
- await connect(res);
+ setRunId(res.runId);
+ await connect(res.connectionDetails);
} catch (e) {
console.error("Failed to start run:", e);
toast.error("Failed to start run. Please try again.");
@@ -88,6 +91,7 @@ function Inner({
const stopRun = useCallback(async () => {
await disconnect();
+ setRunId(null);
}, [disconnect]);
const resolvedConnectionState: ConnectionState = useMemo(() => {
@@ -101,6 +105,7 @@ function Inner({
{
+}): Promise<{ connectionDetails: ConnectionDetails; runId: string }> {
+ const run_id = v4();
const resp = await axios.post(`${getBaseUrl()}/app/run`, {
type: "create_app_run",
graph,
- run_id: v4(),
+ run_id,
});
- return resp.data;
+
+ return {
+ connectionDetails: resp.data.connection_details,
+ runId: run_id,
+ };
}
export async function createDebugConnection({
diff --git a/mcp.example.yaml b/mcp.example.yaml
new file mode 100644
index 00000000..9a7fb838
--- /dev/null
+++ b/mcp.example.yaml
@@ -0,0 +1,8 @@
+servers:
+ - name: blender
+ transport:
+ type: datachannel_proxy
+ local_transport:
+ type: stdio
+ command: uvx
+ args: [blender-mcp]
diff --git a/mcp.yaml b/mcp.yaml
index 8ba44c7d..9a7fb838 100644
--- a/mcp.yaml
+++ b/mcp.yaml
@@ -3,5 +3,6 @@ servers:
transport:
type: datachannel_proxy
local_transport:
- type: sse
- url: http://localhost:9876/sse
+ type: stdio
+ command: uvx
+ args: [blender-mcp]
diff --git a/mcp_proxy_client/pyproject.toml b/mcp_proxy_client/pyproject.toml
new file mode 100644
index 00000000..bbeb6948
--- /dev/null
+++ b/mcp_proxy_client/pyproject.toml
@@ -0,0 +1,64 @@
+[project]
+name = "gabber-mcp-proxy-client"
+version = "0.1.0"
+description = "Proxy client for locally running MCP servers to interact with a remotely running Gabber Engine"
+readme = "README.md"
+requires-python = "==3.12.2"
+
+# Core dependencies needed by both modes
+dependencies = [
+ "aiohttp>=3.12.15",
+ "click>=8.2.1",
+ "gabber-sdk",
+ "mcp>=1.13.1",
+ "pydantic>=2.11.7",
+]
+
+[dependency-groups]
+dev = ["ruff>=0.12.5", "watchfiles>=1.1.0"]
+
+[tool.ruff]
+# Set the target Python version (modern Python, aligns with projects like FastAPI and Pandas)
+target-version = "py312"
+
+# Set line length to match Black's default (88 characters), common in many projects
+line-length = 88
+indent-width = 4
+
+# Include commonly ignored directories to avoid linting irrelevant files
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".hg",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "venv",
+]
+
+src = ["src"]
+
+# Enable a broad set of lint rules for comprehensive checks
+[tool.ruff.lint]
+fixable = ["ALL"]
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+# Use Google-style docstring conventions (common in projects like Pandas)
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.uv.sources]
+gabber-sdk = { path = "../sdks/python", editable = true }
diff --git a/mcp_proxy_client/src/__pycache__/app.cpython-312.pyc b/mcp_proxy_client/src/__pycache__/app.cpython-312.pyc
new file mode 100644
index 00000000..bcf1e612
Binary files /dev/null and b/mcp_proxy_client/src/__pycache__/app.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc b/mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc
new file mode 100644
index 00000000..bec95660
Binary files /dev/null and b/mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/app.py b/mcp_proxy_client/src/app.py
new file mode 100644
index 00000000..1a4f6cbd
--- /dev/null
+++ b/mcp_proxy_client/src/app.py
@@ -0,0 +1,67 @@
+# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
+# SPDX-License-Identifier: SUL-1.0
+
+import asyncio
+import logging
+
+from gabber import ConnectionState, Engine, MCPServer
+from livekit import rtc
+
+from connection import ConnectionProvider
+
+from mcp_proxy import MCPProxy
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+class App:
+ def __init__(self, *, connection_provider: ConnectionProvider, run_id: str):
+ self.run_id = run_id
+ self.engine = Engine(on_connection_state_change=self.on_connection_state_change)
+ self.connection_provider = connection_provider
+
+ async def run(self):
+ dets = await self.connection_provider.get_connection(run_id=self.run_id)
+ proxy_supervisor = ProxySupervisor(room=self.engine._livekit_room)
+
+ await self.engine.connect(connection_details=dets)
+ mcp_servers = await self.engine.list_mcp_servers()
+ for s in mcp_servers:
+ proxy_supervisor.add_server(s)
+
+ await proxy_supervisor.wait_all()
+
+ def on_connection_state_change(self, state: ConnectionState):
+ logger.info(f"Connection state changed: {state}")
+
+
+class ProxySupervisor:
+ def __init__(self, *, room: rtc.Room):
+ self.tasks: list[asyncio.Task] = []
+ self.room = room
+ self._closed = False
+
+ def add_server(self, server: MCPServer):
+ self.tasks.append(asyncio.create_task(self._supervise_server(server)))
+
+ async def _supervise_server(self, server: MCPServer):
+ while not self._closed:
+ proxy = MCPProxy(room=self.room, server=server)
+ try:
+ await proxy.run()
+ except Exception as e:
+ logger.error(f"Error in MCPProxy: {e}", exc_info=True)
+ await asyncio.sleep(2)
+
+ async def wait_all(self):
+ await asyncio.gather(*self.tasks)
+
+ async def aclose(self):
+ self._closed = True
+ for task in self.tasks:
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
diff --git a/mcp_proxy_client/src/connection/__init__.py b/mcp_proxy_client/src/connection/__init__.py
new file mode 100644
index 00000000..f3218c6d
--- /dev/null
+++ b/mcp_proxy_client/src/connection/__init__.py
@@ -0,0 +1,4 @@
+from .connection_provider import ConnectionProvider
+from .local_connection_provider import LocalConnectionProvider
+
+__all__ = ["ConnectionProvider", "LocalConnectionProvider"]
diff --git a/mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..fbf06394
Binary files /dev/null and b/mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc
new file mode 100644
index 00000000..298d3054
Binary files /dev/null and b/mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc
new file mode 100644
index 00000000..fb73d957
Binary files /dev/null and b/mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/connection/connection_provider.py b/mcp_proxy_client/src/connection/connection_provider.py
new file mode 100644
index 00000000..59fa6788
--- /dev/null
+++ b/mcp_proxy_client/src/connection/connection_provider.py
@@ -0,0 +1,7 @@
+from gabber import ConnectionDetails
+from abc import abstractmethod, ABC
+
+
+class ConnectionProvider(ABC):
+ @abstractmethod
+ async def get_connection(self, *, run_id: str) -> ConnectionDetails: ...
diff --git a/mcp_proxy_client/src/connection/local_connection_provider.py b/mcp_proxy_client/src/connection/local_connection_provider.py
new file mode 100644
index 00000000..cae53f9a
--- /dev/null
+++ b/mcp_proxy_client/src/connection/local_connection_provider.py
@@ -0,0 +1,14 @@
+from .connection_provider import ConnectionProvider
+from gabber import ConnectionDetails
+import aiohttp
+
+
+class LocalConnectionProvider(ConnectionProvider):
+ async def get_connection(self, *, run_id: str) -> ConnectionDetails:
+ async with aiohttp.ClientSession() as session:
+ req = {"type": "mcp_proxy_connection", "run_id": run_id}
+ async with session.post(
+ "http://localhost:8001/app/mcp_proxy_connection", json=req
+ ) as response:
+ data = await response.json()
+ return ConnectionDetails(**data["connection_details"])
diff --git a/mcp_proxy_client/src/main.py b/mcp_proxy_client/src/main.py
new file mode 100644
index 00000000..610018a4
--- /dev/null
+++ b/mcp_proxy_client/src/main.py
@@ -0,0 +1,41 @@
+# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
+# SPDX-License-Identifier: SUL-1.0
+
+import asyncio
+import logging
+from typing import Literal
+
+import click
+
+from app import App
+from connection import LocalConnectionProvider, ConnectionProvider
+
+
+@click.group()
+def main_cli():
+ logging.basicConfig(level=logging.INFO)
+
+
+@main_cli.command("start")
+@click.option("--backend", type=click.Choice(["local", "cloud"]), default="local")
+@click.argument("run_id")
+def start(backend: Literal["local", "cloud"], run_id: str):
+ async def run_server():
+ """Start the graph editor server"""
+ conn_provider: ConnectionProvider
+ if backend == "local":
+ conn_provider = LocalConnectionProvider()
+ elif backend == "cloud":
+ raise NotImplementedError("Cloud backend coming soon!")
+
+ p = App(connection_provider=conn_provider, run_id=run_id)
+ try:
+ await p.run()
+ except Exception as e:
+ logging.error(f"Error occurred while running server: {e}", exc_info=True)
+
+ asyncio.run(run_server())
+
+
+if __name__ == "__main__":
+ main_cli()
diff --git a/mcp_proxy_client/src/mcp_proxy/__init__.py b/mcp_proxy_client/src/mcp_proxy/__init__.py
new file mode 100644
index 00000000..9e719d0f
--- /dev/null
+++ b/mcp_proxy_client/src/mcp_proxy/__init__.py
@@ -0,0 +1,3 @@
+from .mcp_proxy import MCPProxy
+
+__all__ = ["MCPProxy"]
diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..b2ee34ed
Binary files /dev/null and b/mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc
new file mode 100644
index 00000000..b220d545
Binary files /dev/null and b/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc
new file mode 100644
index 00000000..bfb18f56
Binary files /dev/null and b/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc differ
diff --git a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py
new file mode 100644
index 00000000..a30de93c
--- /dev/null
+++ b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py
@@ -0,0 +1,49 @@
+# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved.
+# SPDX-License-Identifier: SUL-1.0
+
+import json
+import logging
+
+import mcp.types as types
+from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
+from livekit import rtc
+from mcp.shared.message import SessionMessage
+
+logger = logging.getLogger(__name__)
+
+
+async def datachannel_client_proxy(
+ room: rtc.Room,
+ mcp_name: str,
+ other_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
+ other_write_stream: MemoryObjectSendStream[SessionMessage],
+) -> rtc.Room:
+ topic = f"__mcp__:{mcp_name}"
+
+ def on_message(packet: rtc.DataPacket):
+ if packet.topic != topic:
+ return
+ json_msg = types.JSONRPCMessage.model_validate_json(packet.data)
+ sm = SessionMessage(json_msg)
+ other_write_stream.send_nowait(sm)
+
+ async def read_loop():
+ async with other_read_stream:
+ async for session_message in other_read_stream:
+ logging.debug(
+ f"NEIL------------------------Read session message: {session_message}"
+ )
+ if isinstance(session_message, Exception):
+ logger.error(f"Error in received message: {session_message}")
+ continue
+ msg_dict = session_message.message.model_dump(
+ by_alias=True, mode="json", exclude_none=True
+ )
+ await room.local_participant.publish_data(
+ json.dumps(msg_dict), topic=topic
+ )
+
+ room.on("data_received", on_message)
+ await read_loop()
+ room.off("data_received", on_message)
+ return room
diff --git a/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py
new file mode 100644
index 00000000..4a579b31
--- /dev/null
+++ b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py
@@ -0,0 +1,70 @@
+import asyncio
+import logging
+from contextlib import AsyncExitStack
+import anyio
+
+import mcp
+from gabber import (
+ MCPServer,
+ MCPTransportDatachannelProxy,
+ MCPTransportSSE,
+ MCPTransportSTDIO,
+)
+from livekit import rtc
+from mcp.client.sse import sse_client
+from mcp.client.stdio import stdio_client
+from mcp.shared.message import SessionMessage
+from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
+
+from .datachannel_transport import datachannel_client_proxy
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+class MCPProxy:
+ def __init__(self, *, room: rtc.Room, server: MCPServer):
+ self.room = room
+ self.server = server
+ self.session: mcp.ClientSession | None = None
+ self.exit_stack = AsyncExitStack()
+ self.proxy_task: asyncio.Task | None = None
+
+ async def run(self):
+ if isinstance(self.server.transport, MCPTransportDatachannelProxy):
+ logger.info(f"Starting MCPProxy for {self.server}")
+ local_transport = self.server.transport.local_transport
+ read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
+ write_stream: MemoryObjectSendStream[SessionMessage]
+ if isinstance(local_transport, MCPTransportSSE):
+ read_stream, write_stream = await self.exit_stack.enter_async_context(
+ sse_client("http://localhost:9876")
+ )
+ elif isinstance(local_transport, MCPTransportSTDIO):
+ read_stream, write_stream = await self.exit_stack.enter_async_context(
+ stdio_client(
+ mcp.StdioServerParameters(
+ command=local_transport.command, args=local_transport.args
+ )
+ )
+ )
+ else:
+ raise ValueError(f"Unsupported local transport: {local_transport}")
+
+ await datachannel_client_proxy(
+ room=self.room,
+ mcp_name=self.server.name,
+ other_read_stream=read_stream,
+ other_write_stream=write_stream,
+ )
+
+ logger.info(f"MCPProxy finished {self.server.name}")
+
+ async def aclose(self):
+ await self.exit_stack.aclose()
+ if self.proxy_task:
+ self.proxy_task.cancel()
+ try:
+ await self.proxy_task
+ except asyncio.CancelledError:
+ pass
diff --git a/mcp_proxy_client/uv.lock b/mcp_proxy_client/uv.lock
new file mode 100644
index 00000000..68aa0169
--- /dev/null
+++ b/mcp_proxy_client/uv.lock
@@ -0,0 +1,707 @@
+version = 1
+revision = 2
+requires-python = "==3.12.2"
+
+[[package]]
+name = "aiofiles"
+version = "24.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload_time = "2024-06-24T11:02:03.584Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload_time = "2024-06-24T11:02:01.529Z" },
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload_time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload_time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.12.15"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload_time = "2025-07-29T05:52:32.215Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload_time = "2025-07-29T05:50:46.507Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload_time = "2025-07-29T05:50:48.067Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload_time = "2025-07-29T05:50:49.669Z" },
+ { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload_time = "2025-07-29T05:50:51.368Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload_time = "2025-07-29T05:50:53.628Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload_time = "2025-07-29T05:50:55.394Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload_time = "2025-07-29T05:50:57.202Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload_time = "2025-07-29T05:50:59.192Z" },
+ { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload_time = "2025-07-29T05:51:01.394Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload_time = "2025-07-29T05:51:03.657Z" },
+ { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload_time = "2025-07-29T05:51:05.911Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload_time = "2025-07-29T05:51:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload_time = "2025-07-29T05:51:09.56Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload_time = "2025-07-29T05:51:11.423Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload_time = "2025-07-29T05:51:13.689Z" },
+ { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload_time = "2025-07-29T05:51:15.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload_time = "2025-07-29T05:51:17.239Z" },
+]
+
+[[package]]
+name = "aiohttp-retry"
+version = "2.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", size = 13608, upload_time = "2024-11-06T10:44:54.574Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload_time = "2024-11-06T10:44:52.917Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload_time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload_time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload_time = "2025-08-04T08:54:26.451Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload_time = "2025-08-04T08:54:24.882Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload_time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload_time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload_time = "2025-06-09T23:02:35.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload_time = "2025-06-09T23:00:42.24Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload_time = "2025-06-09T23:00:43.481Z" },
+ { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload_time = "2025-06-09T23:00:44.793Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload_time = "2025-06-09T23:00:46.125Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload_time = "2025-06-09T23:00:47.73Z" },
+ { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload_time = "2025-06-09T23:00:49.742Z" },
+ { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload_time = "2025-06-09T23:00:51.352Z" },
+ { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload_time = "2025-06-09T23:00:52.855Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload_time = "2025-06-09T23:00:54.43Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload_time = "2025-06-09T23:00:56.409Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload_time = "2025-06-09T23:00:58.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload_time = "2025-06-09T23:01:00.015Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload_time = "2025-06-09T23:01:01.474Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload_time = "2025-06-09T23:01:02.961Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload_time = "2025-06-09T23:01:05.095Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload_time = "2025-06-09T23:01:06.54Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload_time = "2025-06-09T23:01:07.752Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload_time = "2025-06-09T23:02:34.204Z" },
+]
+
+[[package]]
+name = "gabber-mcp-proxy-client"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "click" },
+ { name = "gabber-sdk" },
+ { name = "mcp" },
+ { name = "pydantic" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "ruff" },
+ { name = "watchfiles" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiohttp", specifier = ">=3.12.15" },
+ { name = "click", specifier = ">=8.2.1" },
+ { name = "gabber-sdk", editable = "../sdks/python" },
+ { name = "mcp", specifier = ">=1.13.1" },
+ { name = "pydantic", specifier = ">=2.11.7" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "ruff", specifier = ">=0.12.5" },
+ { name = "watchfiles", specifier = ">=1.1.0" },
+]
+
+[[package]]
+name = "gabber-sdk"
+version = "0.2.0"
+source = { editable = "../sdks/python" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "aiohttp-retry" },
+ { name = "livekit" },
+ { name = "numpy" },
+ { name = "pydantic" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiohttp", specifier = "~=3.0" },
+ { name = "aiohttp-retry" },
+ { name = "livekit", specifier = "~=1.0" },
+ { name = "numpy", specifier = "~=2.3.2" },
+ { name = "pydantic", specifier = "~=2.11" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload_time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload_time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload_time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload_time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload_time = "2025-06-24T13:21:05.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload_time = "2025-06-24T13:21:04.772Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload_time = "2025-08-18T17:03:50.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload_time = "2025-08-18T17:03:48.373Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload_time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload_time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "livekit"
+version = "1.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiofiles" },
+ { name = "numpy" },
+ { name = "protobuf" },
+ { name = "types-protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/88/34/955ec1c81440f5c7767b916f7827be64b3d203bc1bb928f39f302d4fac6c/livekit-1.0.12.tar.gz", hash = "sha256:f81ce31d12a5f01ac3e248317d9452896fa300da677650166410a6c40c17d779", size = 311181, upload_time = "2025-07-19T17:29:18.843Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/ce/e89422a9b35832f38b71b3b52b751bc336cf0a51bda8e149eca0437ca38d/livekit-1.0.12-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:aba4f1bc27065ed273a53128af2477372d3f20c5cb272f10bb221425ab932075", size = 10824864, upload_time = "2025-07-19T17:29:07.28Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5a/18e17049d02eb3e5ed6c0c83ca85045517767749c174f96beef29ef95824/livekit-1.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c68c94d8e40ac481ff54956a88ce6632c910b9529d11195f30785c7392446dc3", size = 9531154, upload_time = "2025-07-19T17:29:10.045Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/71/d474d8cf298a2756b39ad2349d2f215fd3502d06becd63181e6c6cd62ad7/livekit-1.0.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c0c937489db946c2fc29ca76f165d946b65411ed73b57e12d9a83934980f5611", size = 10571762, upload_time = "2025-07-19T17:29:12.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/fb/801a5fcb76d29c14f98101194dee22e8ed627ee1072eb4f3dd023070fbd2/livekit-1.0.12-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:34b8aeaba605044dbd5ec6599ed26a342fff20d8a61620fa7f1b03e26b485709", size = 12069549, upload_time = "2025-07-19T17:29:14.496Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/16/a75080702434b8e3cecdd5dbb6d681651b2bca54cb6506e2e3dc89eba858/livekit-1.0.12-py3-none-win_amd64.whl", hash = "sha256:2d75d668ee1ebdebbef6d6a349c6bc44c18c2543f22cbba2a1bdb6f3d125da18", size = 11431532, upload_time = "2025-07-19T17:29:16.916Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload_time = "2025-08-22T09:22:16.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload_time = "2025-08-22T09:22:14.705Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.6.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload_time = "2025-08-11T12:08:48.217Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload_time = "2025-08-11T12:06:53.393Z" },
+ { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload_time = "2025-08-11T12:06:54.555Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload_time = "2025-08-11T12:06:55.672Z" },
+ { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload_time = "2025-08-11T12:06:57.213Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload_time = "2025-08-11T12:06:58.946Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload_time = "2025-08-11T12:07:00.301Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload_time = "2025-08-11T12:07:01.638Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload_time = "2025-08-11T12:07:02.943Z" },
+ { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload_time = "2025-08-11T12:07:04.564Z" },
+ { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload_time = "2025-08-11T12:07:05.914Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload_time = "2025-08-11T12:07:08.301Z" },
+ { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload_time = "2025-08-11T12:07:10.248Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload_time = "2025-08-11T12:07:11.928Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload_time = "2025-08-11T12:07:13.244Z" },
+ { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload_time = "2025-08-11T12:07:14.57Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload_time = "2025-08-11T12:07:15.904Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload_time = "2025-08-11T12:07:17.045Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload_time = "2025-08-11T12:07:18.328Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload_time = "2025-08-11T12:08:46.891Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload_time = "2025-09-09T16:54:12.543Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload_time = "2025-09-09T15:56:29.966Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload_time = "2025-09-09T15:56:32.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload_time = "2025-09-09T15:56:34.175Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload_time = "2025-09-09T15:56:36.149Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload_time = "2025-09-09T15:56:40.548Z" },
+ { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload_time = "2025-09-09T15:56:43.343Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload_time = "2025-09-09T15:56:46.141Z" },
+ { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload_time = "2025-09-09T15:56:49.844Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload_time = "2025-09-09T15:56:52.499Z" },
+ { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload_time = "2025-09-09T15:56:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload_time = "2025-09-09T15:56:56.541Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload_time = "2025-06-09T22:56:06.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload_time = "2025-06-09T22:54:30.551Z" },
+ { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload_time = "2025-06-09T22:54:32.296Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload_time = "2025-06-09T22:54:33.929Z" },
+ { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload_time = "2025-06-09T22:54:35.186Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload_time = "2025-06-09T22:54:36.708Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload_time = "2025-06-09T22:54:38.062Z" },
+ { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload_time = "2025-06-09T22:54:39.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload_time = "2025-06-09T22:54:41.565Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload_time = "2025-06-09T22:54:43.038Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload_time = "2025-06-09T22:54:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload_time = "2025-06-09T22:54:46.243Z" },
+ { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload_time = "2025-06-09T22:54:47.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload_time = "2025-06-09T22:54:48.982Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload_time = "2025-06-09T22:54:50.424Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload_time = "2025-06-09T22:54:52.072Z" },
+ { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload_time = "2025-06-09T22:54:53.234Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload_time = "2025-06-09T22:56:04.484Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.32.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/df/fb4a8eeea482eca989b51cffd274aac2ee24e825f0bf3cbce5281fa1567b/protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2", size = 440614, upload_time = "2025-08-14T21:21:25.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/18/df8c87da2e47f4f1dcc5153a81cd6bca4e429803f4069a299e236e4dd510/protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741", size = 424409, upload_time = "2025-08-14T21:21:12.366Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/59/0a820b7310f8139bd8d5a9388e6a38e1786d179d6f33998448609296c229/protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e", size = 435735, upload_time = "2025-08-14T21:21:15.046Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/5b/0d421533c59c789e9c9894683efac582c06246bf24bb26b753b149bd88e4/protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0", size = 426449, upload_time = "2025-08-14T21:21:16.687Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/7b/607764ebe6c7a23dcee06e054fd1de3d5841b7648a90fd6def9a3bb58c5e/protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1", size = 322869, upload_time = "2025-08-14T21:21:18.282Z" },
+ { url = "https://files.pythonhosted.org/packages/40/01/2e730bd1c25392fc32e3268e02446f0d77cb51a2c3a8486b1798e34d5805/protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c", size = 322009, upload_time = "2025-08-14T21:21:19.893Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f2/80ffc4677aac1bc3519b26bc7f7f5de7fce0ee2f7e36e59e27d8beb32dd1/protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783", size = 169287, upload_time = "2025-08-14T21:21:23.515Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload_time = "2025-06-14T08:33:17.137Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload_time = "2025-06-14T08:33:14.905Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload_time = "2025-04-23T18:33:52.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload_time = "2025-04-23T18:31:25.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload_time = "2025-04-23T18:31:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload_time = "2025-04-23T18:31:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload_time = "2025-04-23T18:31:31.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload_time = "2025-04-23T18:31:32.514Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload_time = "2025-04-23T18:31:33.958Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload_time = "2025-04-23T18:31:39.095Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload_time = "2025-04-23T18:31:41.034Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload_time = "2025-04-23T18:31:42.757Z" },
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload_time = "2025-04-23T18:31:44.304Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload_time = "2025-04-23T18:31:45.891Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload_time = "2025-04-23T18:31:47.819Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload_time = "2025-04-23T18:31:49.635Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload_time = "2025-04-23T18:31:51.609Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload_time = "2025-06-24T13:26:46.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload_time = "2025-06-24T13:26:45.485Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload_time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload_time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload_time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload_time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload_time = "2025-07-14T20:13:24.682Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload_time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload_time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.27.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload_time = "2025-08-27T12:16:36.024Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload_time = "2025-08-27T12:13:10.233Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload_time = "2025-08-27T12:13:11.65Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload_time = "2025-08-27T12:13:13.008Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload_time = "2025-08-27T12:13:14.368Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload_time = "2025-08-27T12:13:15.774Z" },
+ { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload_time = "2025-08-27T12:13:17.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload_time = "2025-08-27T12:13:18.704Z" },
+ { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload_time = "2025-08-27T12:13:20.089Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload_time = "2025-08-27T12:13:21.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload_time = "2025-08-27T12:13:22.789Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload_time = "2025-08-27T12:13:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload_time = "2025-08-27T12:13:25.557Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload_time = "2025-08-27T12:13:26.967Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload_time = "2025-08-27T12:13:28.326Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload_time = "2025-08-27T12:13:29.71Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload_time = "2025-09-04T16:50:18.273Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload_time = "2025-09-04T16:49:18.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload_time = "2025-09-04T16:49:23.043Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload_time = "2025-09-04T16:49:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload_time = "2025-09-04T16:49:29.056Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload_time = "2025-09-04T16:49:32.07Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload_time = "2025-09-04T16:49:35.148Z" },
+ { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload_time = "2025-09-04T16:49:38.892Z" },
+ { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload_time = "2025-09-04T16:49:42.732Z" },
+ { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload_time = "2025-09-04T16:49:46.434Z" },
+ { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload_time = "2025-09-04T16:49:49.931Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload_time = "2025-09-04T16:49:53.465Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload_time = "2025-09-04T16:49:56.882Z" },
+ { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload_time = "2025-09-04T16:49:59.729Z" },
+ { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload_time = "2025-09-04T16:50:02.591Z" },
+ { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload_time = "2025-09-04T16:50:05.8Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload_time = "2025-09-04T16:50:09.121Z" },
+ { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload_time = "2025-09-04T16:50:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload_time = "2025-09-04T16:50:15.737Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload_time = "2025-07-27T09:07:44.565Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload_time = "2025-07-27T09:07:43.268Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.47.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload_time = "2025-08-24T13:36:42.122Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload_time = "2025-08-24T13:36:40.887Z" },
+]
+
+[[package]]
+name = "types-protobuf"
+version = "6.30.2.20250822"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/68/0c7144be5c6dc16538e79458839fc914ea494481c7e64566de4ecc0c3682/types_protobuf-6.30.2.20250822.tar.gz", hash = "sha256:faacbbe87bd8cba4472361c0bd86f49296bd36f7761e25d8ada4f64767c1bde9", size = 62379, upload_time = "2025-08-22T03:01:56.572Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/64/b926a6355993f712d7828772e42b9ae942f2d306d25072329805c374e729/types_protobuf-6.30.2.20250822-py3-none-any.whl", hash = "sha256:5584c39f7e36104b5f8bdfd31815fa1d5b7b3455a79ddddc097b62320f4b1841", size = 76523, upload_time = "2025-08-22T03:01:55.157Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload_time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload_time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload_time = "2025-05-21T18:55:23.885Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload_time = "2025-05-21T18:55:22.152Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.35.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload_time = "2025-06-28T16:15:46.058Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload_time = "2025-06-15T19:06:59.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload_time = "2025-06-15T19:05:24.516Z" },
+ { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload_time = "2025-06-15T19:05:25.469Z" },
+ { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload_time = "2025-06-15T19:05:26.494Z" },
+ { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload_time = "2025-06-15T19:05:27.466Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload_time = "2025-06-15T19:05:28.548Z" },
+ { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload_time = "2025-06-15T19:05:29.997Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload_time = "2025-06-15T19:05:31.172Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload_time = "2025-06-15T19:05:32.299Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload_time = "2025-06-15T19:05:33.415Z" },
+ { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload_time = "2025-06-15T19:05:34.534Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload_time = "2025-06-15T19:05:35.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload_time = "2025-06-15T19:05:36.559Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload_time = "2025-06-15T19:05:37.5Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload_time = "2025-06-10T00:46:09.923Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload_time = "2025-06-10T00:43:44.369Z" },
+ { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload_time = "2025-06-10T00:43:46.295Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload_time = "2025-06-10T00:43:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload_time = "2025-06-10T00:43:49.924Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload_time = "2025-06-10T00:43:51.7Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload_time = "2025-06-10T00:43:53.494Z" },
+ { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload_time = "2025-06-10T00:43:55.766Z" },
+ { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload_time = "2025-06-10T00:43:58.056Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload_time = "2025-06-10T00:43:59.773Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload_time = "2025-06-10T00:44:02.051Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload_time = "2025-06-10T00:44:04.196Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload_time = "2025-06-10T00:44:06.527Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload_time = "2025-06-10T00:44:08.379Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload_time = "2025-06-10T00:44:10.51Z" },
+ { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload_time = "2025-06-10T00:44:12.834Z" },
+ { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload_time = "2025-06-10T00:44:14.731Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload_time = "2025-06-10T00:44:16.716Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload_time = "2025-06-10T00:46:07.521Z" },
+]
diff --git a/sdks/python/example/pyproject.toml b/sdks/python/example/pyproject.toml
index f37c7515..d55d212f 100644
--- a/sdks/python/example/pyproject.toml
+++ b/sdks/python/example/pyproject.toml
@@ -10,9 +10,6 @@ requires-python = "==3.12.2"
dependencies = [
"aiofiles>=24.1.0",
"aiohttp>=3.12.15",
- "gabber-sdk",
+ "gabber-sdk>=0.2.0",
"numpy>=2.3.2",
]
-
-[tool.uv.sources]
-gabber-sdk = { path = "../", editable = true }
diff --git a/sdks/python/example/uv.lock b/sdks/python/example/uv.lock
index 526ba8d1..11af34fe 100644
--- a/sdks/python/example/uv.lock
+++ b/sdks/python/example/uv.lock
@@ -138,14 +138,14 @@ dependencies = [
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "aiohttp", specifier = ">=3.12.15" },
- { name = "gabber-sdk", editable = "../" },
+ { name = "gabber-sdk", specifier = ">=0.2.0" },
{ name = "numpy", specifier = ">=2.3.2" },
]
[[package]]
name = "gabber-sdk"
version = "0.2.0"
-source = { editable = "../" }
+source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "aiohttp-retry" },
@@ -153,14 +153,9 @@ dependencies = [
{ name = "numpy" },
{ name = "pydantic" },
]
-
-[package.metadata]
-requires-dist = [
- { name = "aiohttp", specifier = "~=3.0" },
- { name = "aiohttp-retry" },
- { name = "livekit", specifier = "~=1.0" },
- { name = "numpy", specifier = "~=2.3.2" },
- { name = "pydantic", specifier = "~=2.11" },
+sdist = { url = "https://files.pythonhosted.org/packages/c1/39/8abee69d27999f54eb8153bee4a1bc4b10b547c17a152d96847a2ba1b61d/gabber_sdk-0.2.0.tar.gz", hash = "sha256:c3747fdf620d7f8a1ef8b5a1a0011e6066ade38a66c34d19bb4024127601e92a", size = 80860, upload_time = "2025-09-09T18:08:08.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/9d/a97424a1cb31ee5e3d54055085a45218769875ef2cdb6d71803f1f1dd74a/gabber_sdk-0.2.0-py3-none-any.whl", hash = "sha256:a12c3474ed7a81fb341f64e756a768f4927b5e777414cde516ac34d3a1d263e4", size = 14180, upload_time = "2025-09-09T18:08:08.126Z" },
]
[[package]]
diff --git a/sdks/python/gabber/__init__.py b/sdks/python/gabber/__init__.py
index 08730d24..4eda30b1 100644
--- a/sdks/python/gabber/__init__.py
+++ b/sdks/python/gabber/__init__.py
@@ -17,6 +17,10 @@
PadValueInteger,
PadValueTrigger,
PadValueVideoClip,
+ MCPServer,
+ MCPTransportDatachannelProxy,
+ MCPTransportSSE,
+ MCPTransportSTDIO,
)
from .media import (
AudioFrame,
@@ -42,6 +46,10 @@
"PadValueInteger",
"PadValueTrigger",
"PadValueVideoClip",
+ "MCPServer",
+ "MCPTransportDatachannelProxy",
+ "MCPTransportSSE",
+ "MCPTransportSTDIO",
"AudioFrame",
"VideoFrame",
"VideoFormat",
diff --git a/sdks/python/gabber/engine/engine.py b/sdks/python/gabber/engine/engine.py
index b5807daf..924f39c6 100644
--- a/sdks/python/gabber/engine/engine.py
+++ b/sdks/python/gabber/engine/engine.py
@@ -42,14 +42,13 @@ def __init__(
Callable[[types.ConnectionState], None]
] = None,
):
- logging.debug("Creating new Engine instance")
- self._livekit_room: rtc.Room = rtc.Room()
+ self._livekit_room = rtc.Room()
+ self.setup_room_event_listeners()
self._on_connection_state_change = on_connection_state_change
self._last_emitted_connection_state: types.ConnectionState = "disconnected"
self._runtime_request_id_counter: int = 1
- self._pending_requests: Dict[str, Dict[str, Callable]] = {}
+ self._pending_futs: Dict[str, Dict[str, Callable]] = {}
self._pad_value_handlers: Dict[str, List[Callable[[Any], None]]] = {}
- self.setup_room_event_listeners()
@property
def connection_state(self) -> types.ConnectionState:
@@ -129,28 +128,39 @@ async def subscribe_to_node(self, *, output_or_publish_node: str):
async def list_mcp_servers(self) -> List[runtime.MCPServer]:
payload = runtime.RuntimeRequestPayloadListMCPServers(type="list_mcp_servers")
- response = await self.runtime_request(payload)
- if response.type != "list_mcp_servers":
- raise ValueError("Unexpected response type")
- return response.servers
+ retries = 3
+ last_exception = None
+ for attempt in range(retries):
+ try:
+ response = await self.runtime_request(payload)
+ if response.type == "list_mcp_servers":
+ return response.servers
+ except Exception as e:
+ last_exception = e
+
+ if last_exception:
+ raise last_exception
+ raise ValueError("Unexpected response type")
async def runtime_request(
- self, payload: types.RuntimeRequestPayload
+ self, payload: types.RuntimeRequestPayload, timeout: float = 2.0
) -> types.RuntimeResponsePayload:
topic = "runtime_api"
request_id = str(self._runtime_request_id_counter)
self._runtime_request_id_counter += 1
req = runtime.RuntimeRequest(req_id=request_id, payload=payload, type="request")
- loop = asyncio.get_running_loop()
- future: asyncio.Future[types.RuntimeResponsePayload] = loop.create_future()
- self._pending_requests[request_id] = {
- "res": lambda response: future.set_result(response),
- "rej": lambda error: future.set_exception(ValueError(error)),
- }
- await self._livekit_room.local_participant.publish_data(
- req.model_dump_json(), topic=topic, destination_identities=["gabber-engine"]
- )
- return await future
+ future = asyncio.Future[types.RuntimeResponsePayload]()
+ self._pending_futs[request_id] = future
+ try:
+ await self._livekit_room.local_participant.publish_data(
+ req.model_dump_json(),
+ topic=topic,
+ destination_identities=["gabber-engine"],
+ )
+ return await asyncio.wait_for(future, timeout=timeout)
+ except Exception as e:
+ future.set_exception(e)
+ raise
def get_source_pad(self, node_id: str, pad_id: str) -> "SourcePad":
return SourcePad(
@@ -168,26 +178,18 @@ def get_property_pad(self, node_id: str, pad_id: str) -> "PropertyPad":
)
def setup_room_event_listeners(self) -> None:
- def on_connected():
- self._emit_connection_state_change()
-
- self._livekit_room.on("connected", on_connected)
-
- def on_disconnected(reason: Any):
+ def on_connected(state: rtc.ConnectionState):
self._emit_connection_state_change()
- self._livekit_room.on("disconnected", on_disconnected)
-
def on_participant_connected(participant: rtc.RemoteParticipant):
self._emit_connection_state_change()
- self._livekit_room.on("participant_connected", on_participant_connected)
-
def on_participant_disconnected(participant: rtc.RemoteParticipant):
self._emit_connection_state_change()
+ self._livekit_room.on("connection_state_changed", on_connected)
+ self._livekit_room.on("participant_connected", on_participant_connected)
self._livekit_room.on("participant_disconnected", on_participant_disconnected)
-
self._livekit_room.on("data_received", self._on_data)
def _add_pad_value_handler(
@@ -229,14 +231,14 @@ def _on_data(
payload = resp.payload
if resp.error is not None:
logging.error("Error in request: %s", msg["error"])
- pending_request = self._pending_requests.get(msg["req_id"])
+ pending_request = self._pending_futs.get(msg["req_id"])
if pending_request:
- pending_request["rej"](msg["error"])
+ pending_request.set_exception(Exception(msg["error"]))
else:
- pending_request = self._pending_requests.get(msg["req_id"])
+ pending_request = self._pending_futs.get(msg["req_id"])
if pending_request:
- pending_request["res"](payload)
- self._pending_requests.pop(msg["req_id"], None)
+ pending_request.set_result(payload)
+ self._pending_futs.pop(msg["req_id"], None)
elif msg["type"] == "event":
resp = runtime.RuntimeEvent.model_validate(msg)
payload = resp.payload
diff --git a/sdks/python/gabber/generated/runtime.py b/sdks/python/gabber/generated/runtime.py
index ffa2c0d1..86dbb816 100644
--- a/sdks/python/gabber/generated/runtime.py
+++ b/sdks/python/gabber/generated/runtime.py
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: runtime.json
-# timestamp: 2025-09-08T23:21:21+00:00
+# timestamp: 2025-09-10T05:08:08+00:00
from __future__ import annotations
@@ -14,6 +14,12 @@ class MCPTransportSSE(BaseModel):
url: Annotated[str, Field(title='Url')]
+class MCPTransportSTDIO(BaseModel):
+ type: Annotated[Literal['stdio'], Field(title='Type')] = 'stdio'
+ command: Annotated[str, Field(title='Command')]
+ args: Annotated[list[str], Field(title='Args')]
+
+
class PadValueAudioClip(BaseModel):
type: Annotated[Literal['audio_clip'], Field(title='Type')] = 'audio_clip'
transcript: Annotated[str, Field(title='Transcript')]
@@ -128,7 +134,10 @@ class MCPTransportDatachannelProxy(BaseModel):
type: Annotated[Literal['datachannel_proxy'], Field(title='Type')] = (
'datachannel_proxy'
)
- local_transport: MCPTransportSSE
+ local_transport: Annotated[
+ Union[MCPTransportSTDIO, MCPTransportSSE],
+ Field(discriminator='type', title='Local Transport'),
+ ]
class RuntimeEvent(BaseModel):
@@ -164,7 +173,8 @@ class RuntimeRequest(BaseModel):
class MCPServer(BaseModel):
name: Annotated[str, Field(title='Name')]
transport: Annotated[
- MCPTransportDatachannelProxy, Field(discriminator='type', title='Transport')
+ Union[MCPTransportDatachannelProxy, MCPTransportSTDIO],
+ Field(discriminator='type', title='Transport'),
]
diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml
index adaf59df..5d1b12ad 100644
--- a/sdks/python/pyproject.toml
+++ b/sdks/python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "gabber-sdk"
-version = "0.2.0"
+version = "0.2.1"
description = "Python client SDK for interacting with the Gabber Engine"
readme = "README.md"
requires-python = ">=3.11"