diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e52a94c
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+.venv
+.env
+.git
+.gitlab-ci.yml
+.gitignore
+__pycache__
+gl-sast-report.json
+
+Dockerfile
+README.md
+dev_tests
+data/
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
new file mode 100644
index 0000000..5a67e5f
--- /dev/null
+++ b/.github/workflows/docker.yaml
@@ -0,0 +1,78 @@
+#
+name: Create and publish a Docker image for Litmus MCP Server
+
+on:
+ push:
+ branches: ['main']
+ tags:
+ - "v*.*.*"
+ pull_request:
+ branches: ['main']
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+ PLATFORMS: "linux/amd64"
+
+jobs:
+ docker-build:
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
+ name: Build Docker image (ghcr.io/litmusautomation/litmus-mcp-server)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Log in to the Container registry
+ if: ${{ github.event_name != 'pull_request' }}
+ uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+ env:
+ DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=sha
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
+ with:
+ platforms: ${{ env.PLATFORMS }}
+
+ - name: Build and push Docker image
+ id: push
+ uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
+ with:
+ platforms: ${{ env.PLATFORMS }}
+ push: ${{ github.event_name != 'pull_request' }}
+ sbom: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ annotations: ${{ steps.meta.outputs.annotations }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # - name: Generate artifact attestation
+ # if: ${{ github.event_name != 'pull_request' }}
+ # uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
+ # with:
+ # subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
+ # subject-digest: ${{ steps.push.outputs.digest }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..edbb24a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+
+# local env
+.env
+.idea
+dev_tests
+.directory
\ No newline at end of file
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3a4753a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+FROM python:3.12-slim
+LABEL authors="Litmus Automation, Inc."
+
+
+RUN apt-get update && apt-get install -y \
+ build-essential curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
+COPY --from=ghcr.io/astral-sh/uv:latest /uvx /bin/uvx
+WORKDIR /app
+
+COPY . .
+RUN uv venv && uv sync --all-groups
+
+ENV PATH="/app/.venv/bin:$PATH"
+
+#CMD python src/server.py --transport=sse & python src/webclient.py src/server.py && wait
+RUN chmod +x run.sh
+CMD ["./run.sh"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3097b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Litmus MCP Server
+
+The official [Litmus Automation](https://litmus.io) **Model Context Protocol (MCP) Server** enables LLMs and intelligent systems to interact with [Litmus Edge](https://litmus.io/products/litmus-edge) for device configuration, monitoring, and management. It is built on top of the MCP SDK and adheres to the [Model Context Protocol spec](https://modelcontextprotocol.io/).
+
+
+
+
+
+
+
+
+## Table of Contents
+
+- [Getting Started](#getting-started)
+ - [Quick Launch (Docker)](#quick-launch-docker)
+ - [Cursor IDE Setup](#cursor-ide-setup)
+- [API](#api)
+- [Usage](#usage)
+ - [Server-Sent Events (SSE)](#server-sent-events-sse)
+- [Litmus Central](#litmus-central)
+- [Integrations](#integrations)
+ - [Cursor IDE](#cursor-ide)
+ - [Claude Desktop](#claude-desktop)
+ - [VS Code / Copilot](#vs-code--copilot)
+ - [Windsurf](#windsurf)
+
+---
+
+## Getting Started
+
+### Quick Launch (Docker)
+
+Run the server in Docker:
+
+```bash
+docker run -d --name litmus-mcp-server -p 8000:8000 ghcr.io/litmusautomation/litmus-mcp-server/mcp:latest
+```
+
+### Cursor IDE Setup
+
+Example `mcp.json` configuration:
+
+```json
+{
+ "mcpServers": {
+ "litmus-mcp-server": {
+ "url": "http://:8000/sse"
+ }
+ }
+}
+```
+
+See the [Cursor docs](https://docs.cursor.com/context/model-context-protocol) for more info.
+
+---
+
+## API
+
+| Category | Function Name | Description |
+|---------------------------|----------------------------------------|-------------|
+| **Edge System Config** | `get_current_environment_config` | Get current environment configuration used for Litmus Edge connectivity. |
+| | `update_environment_config` | Update environment variable config for connecting to Litmus Edge. |
+| | `get_current_config` | Retrieve current Litmus Edge instance configuration. |
+| | `update_config` | Update configuration of the device or container running Litmus Edge. |
+| **DeviceHub** | `get_litmusedge_driver_list` | List supported Litmus Edge drivers. |
+| | `get_devicehub_devices` | List devices configured in DeviceHub. |
+| | `get_devicehub_device_tags` | Retrieve tags for a specific DeviceHub device. |
+| | `get_current_value_of_devicehub_tag` | Get current value of a specific device tag. |
+| | `create_devicehub_device` | Register a new DeviceHub device. Supports various protocols and templates for register-based data polling. |
+| **Device Identity** | `get_litmusedge_friendly_name` | Retrieve the user-friendly name of the device. |
+| | `set_litmusedge_friendly_name` | Assign or update the friendly name. |
+| **LEM Integration** | `get_cloud_activation_status` | Check cloud activation and Litmus Edge Manager (LEM) connection status. |
+| **Docker Management** | `get_all_containers_on_litmusedge` | List all containers on Litmus Edge. |
+| | `run_docker_container_on_litmusedge` | Launch a Docker container via Litmus Edge Marketplace (not the MCP host). |
+| **Topic Subscription** | `get_current_value_on_topic` | Subscribe to current values on a Litmus Edge topic. Use global `NATS_STATUS = False` to unsubscribe. |
+| | `get_multiple_values_from_topic` | Retrieve multiple values from a topic for plotting or batch access. |
+
+---
+
+## Usage
+
+### Server-Sent Events (SSE)
+
+This server supports the [MCP SSE transport](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse) for real-time communication.
+
+- **Client endpoint:** `http://:8000/sse`
+- **Default binding:** `0.0.0.0:8000/sse`
+- **Communication:**
+ - Server → Client: Streamed via SSE
+ - Client → Server: HTTP POST
+
+---
+
+## Litmus Central
+
+Download or try Litmus Edge via [Litmus Central](https://central.litmus.io).
+
+---
+
+## Integrations
+
+### Cursor IDE
+
+Add to `~/.cursor/mcp.json` or `.cursor/mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "litmus-mcp-server": {
+ "url": "http://:8000/sse"
+ }
+ }
+}
+```
+
+[Cursor docs](https://docs.cursor.com/context/model-context-protocol)
+
+---
+
+### Claude Desktop
+
+Add to `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "litmus-mcp-server": {
+ "url": "http://:8000/sse"
+ }
+ }
+}
+```
+
+[Anthropic Docs](https://docs.anthropic.com/en/docs/agents-and-tools/mcp)
+
+---
+
+### VS Code / GitHub Copilot
+
+#### Manual Configuration
+
+In VS Code:
+Open User Settings (JSON) → Add:
+
+```json
+{
+ "mcpServers": {
+ "litmus-mcp-server": {
+ "url": "http://:8000/sse"
+ }
+ }
+}
+```
+
+Or use `.vscode/mcp.json` in your project.
+
+[VS Code MCP Docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
+
+---
+
+### Windsurf
+
+Add to `~/.codeium/windsurf/mcp_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "litmus-mcp-server": {
+ "url": "http://:8000/sse"
+ }
+ }
+}
+```
+
+[Windsurf MCP Docs](https://docs.windsurf.com/windsurf/mcp)
+
+---
+
+© 2025 Litmus Automation, Inc. All rights reserved.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..006c36e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,47 @@
+[project]
+name = "litmus-mcp-server"
+version = "0.1.0"
+description = "Litmus MCP Server and client combo"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.115.12",
+ "jinja2>=3.1.6",
+ "litmussdk",
+ "mcp[cli]>=1.6.0",
+ "nats-py>=2.10.0",
+ "numpy>=2.2.5",
+ "python-multipart>=0.0.20",
+]
+
+[dependency-groups]
+lint = [
+ "black>=25.1.0",
+ "radon>=6.0.1",
+ "ruff>=0.11.4",
+]
+llm-sdks = [
+ "anthropic>=0.49.0",
+ "openai-agents>=0.0.13",
+]
+test = []
+
+[tool.uv.sources]
+litmussdk = { url = "https://github.com/litmusautomation/litmus-sdk-releases/releases/download/1.0.0/litmussdk-1.0.0-py3-none-any.whl" }
+
+[tool.ruff]
+exclude = [
+ ".venv",
+ "venv",
+ "site-packages",
+ "build",
+ "dist",
+ ".pytest_cache",
+ ".ruff_cache",
+ "__init__.py",
+ "tester"
+]
+line-length = 88
+indent-width = 4
+[tool.ruff.lint.pydocstyle]
+convention = "google"
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000..e9f28d4
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -e
+
+# Start server
+mcp run src/server.py --transport=sse &
+SERVER_PID=$!
+
+# Start web client
+python src/web_client.py src/server.py &
+CLIENT_PID=$!
+
+# Wait for both
+wait $SERVER_PID $CLIENT_PID
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/cli_client.py b/src/cli_client.py
new file mode 100644
index 0000000..a67dbce
--- /dev/null
+++ b/src/cli_client.py
@@ -0,0 +1,24 @@
+import asyncio
+from utils import mcp_env_loader
+from client_utils import MCPClient
+
+mcp_env_loader()
+
+
+async def main():
+ if len(sys.argv) < 2:
+ print("Usage: python cli_client.py ")
+ sys.exit(1)
+
+ client = MCPClient()
+ try:
+ await client.connect_to_server(sys.argv[1])
+ await client.chat_loop()
+ finally:
+ await client.cleanup()
+
+
+if __name__ == "__main__":
+ import sys
+
+ asyncio.run(main())
diff --git a/src/client_utils.py b/src/client_utils.py
new file mode 100644
index 0000000..98b518f
--- /dev/null
+++ b/src/client_utils.py
@@ -0,0 +1,275 @@
+from typing import Optional, Iterable, cast
+from contextlib import AsyncExitStack
+from mcp import ClientSession, StdioServerParameters, stdio_client
+
+from anthropic import Anthropic
+from anthropic.types import MessageParam, ToolParam
+
+from agents import Agent, Runner, gen_trace_id, trace
+from agents.mcp import MCPServerStdio
+from agents import ModelSettings
+
+_default_anthropic_model = "claude-3-7-sonnet-20250219"
+_anthropic_display_name = "Claude Sonnet"
+_openai_display_name = "OpenAI GPT-4o"
+
+
+class MCPClient:
+ def __init__(self):
+ # Initialize session and client objects
+ self.session: Optional[ClientSession] = None
+ self.exit_stack = AsyncExitStack()
+ self.anthropic = None
+ self.stdio = None
+ self.write = None
+ self.server_params = None
+ self.model_used = None
+ self.current_full_response = None
+
+ async def connect_to_server(self, server_script_path: str):
+ """Connect to an MCP server
+
+ Args:
+ server_script_path: Path to the server script (.py or .js)
+ """
+ if self.server_params is None:
+ self.server_params = {}
+
+ is_python = server_script_path.endswith(".py")
+ is_js = server_script_path.endswith(".js")
+ if not (is_python or is_js):
+ raise ValueError("Server script must be a .py or .js file")
+
+ command = "python" if is_python else "node"
+
+ self.server_params["command"] = command
+ self.server_params["args"] = [server_script_path]
+ server_params = StdioServerParameters(
+ command=command, args=[server_script_path], env=None
+ )
+
+ stdio_transport = await self.exit_stack.enter_async_context(
+ stdio_client(server_params)
+ )
+ self.stdio, self.write = stdio_transport
+ self.session = await self.exit_stack.enter_async_context(
+ ClientSession(self.stdio, self.write)
+ )
+
+ await self.session.initialize()
+
+ # List available tools
+ response = await self.session.list_tools()
+ tools = response.tools
+ print("\nConnected to server with tools:", [tool.name for tool in tools])
+
+ async def anthropic_creator(self, query, conversation_history):
+ if self.anthropic is None:
+ self.anthropic = Anthropic()
+
+ # Use provided conversation history or create new messages list
+ if conversation_history:
+ messages = conversation_history.copy()
+ else:
+ messages = []
+
+ # Add the user's current query
+ messages.append({"role": "user", "content": query})
+
+ response = await self.session.list_tools()
+ available_tools = [
+ {
+ "name": tool.name,
+ "description": tool.description,
+ "input_schema": tool.inputSchema,
+ }
+ for tool in response.tools
+ ]
+ self.model_used = _anthropic_display_name
+
+ return messages, available_tools
+
+ async def process_query_anthropic(
+ self,
+ query: str,
+ conversation_history=None,
+ max_tokens: int = 4096,
+ anthropic_model_used=None,
+ ) -> str:
+ """
+ Process a query using Claude and available tools with conversation history
+
+ Args:
+ query: Query to process
+ conversation_history: Optional list of previous messages
+ max_tokens: Maximum tokens for response (default: 4096)
+ anthropic_model_used: default model is claude-3-7-sonnet-20250219
+ """
+ model = anthropic_model_used or _default_anthropic_model
+ messages, available_tools = await self.anthropic_creator(
+ query, conversation_history
+ )
+
+ converted_messages = cast(Iterable[MessageParam], messages)
+ converted_available_tools = cast(Iterable[ToolParam], available_tools)
+
+ # Claude API call with conversation history
+ response = self.anthropic.messages.create(
+ model=model,
+ max_tokens=max_tokens,
+ messages=converted_messages,
+ tools=converted_available_tools,
+ )
+
+ # Process response and handle tool calls
+ final_text = []
+
+ for content in response.content:
+ if content.type == "text":
+ final_text.append(content.text)
+ elif content.type == "tool_use":
+ tool_name = content.name
+ tool_args = content.input
+ try:
+ tool_args = vars(tool_args)
+ except TypeError:
+ tool_args = tool_args
+
+ # Execute tool call
+ result = await self.session.call_tool(tool_name, tool_args)
+ final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
+
+ # Continue conversation with tool results
+ if hasattr(content, "text") and content.text:
+ messages.append({"role": "assistant", "content": content.text})
+ messages.append({"role": "user", "content": result.content})
+ converted_messages = cast(Iterable[MessageParam], messages)
+
+ # Get next response from Claude
+ response = self.anthropic.messages.create(
+ model=model,
+ max_tokens=max_tokens,
+ messages=converted_messages,
+ tools=converted_available_tools,
+ )
+
+ final_text.append(response.content[0].text)
+
+ return "\n".join(final_text)
+
+ async def chat_loop(self):
+ """Run an interactive chat loop with conversation history (max 5 messages)"""
+ conversation_history = []
+ max_history = 5 # Keep only the last 5 message pairs (10 messages total)
+
+ print("Interactive chat mode (type 'quit' to exit, 'clear' to reset history)")
+
+ while True:
+ try:
+ query = input("\nQuery: ").strip()
+
+ if query.lower() == "quit":
+ break
+ elif query.lower() == "clear":
+ conversation_history = []
+ print("Conversation history cleared")
+ continue
+
+ # Process the query with conversation history
+ response = await self.process_query_anthropic(
+ query, conversation_history=conversation_history
+ )
+ print("\n" + response)
+
+ # Update conversation history
+ conversation_history.append({"role": "user", "content": query})
+ conversation_history.append({"role": "assistant", "content": response})
+
+ # Trim history to keep only the most recent message pairs
+ if len(conversation_history) > max_history * 2:
+ conversation_history = conversation_history[-max_history * 2 :]
+
+ except Exception as e:
+ print(f"\nError: {str(e)}")
+
+ async def process_streaming_query(
+ self,
+ query: str,
+ conversation_history=None,
+ max_tokens: int = 4096,
+ anthropic_model_used=None,
+ ):
+ """
+ Process a query using Claude with streaming responses.
+
+ Args:
+ query: Query to process
+ conversation_history: Optional list of previous messages
+ max_tokens: Maximum tokens for response
+ anthropic_model_used: default model is claude-3-7-sonnet-20250219
+
+ Yields:
+ Chunks of text as they become available
+ """
+ model = anthropic_model_used or _default_anthropic_model
+ messages, available_tools = await self.anthropic_creator(
+ query, conversation_history
+ )
+ messages, available_tools = await self.anthropic_creator(
+ query, conversation_history
+ )
+
+ # Initialize the full response tracker
+ self.current_full_response = ""
+
+ def stream_chunks():
+ with self.anthropic.messages.stream(
+ model=model,
+ max_tokens=max_tokens,
+ messages=messages,
+ tools=available_tools,
+ ) as stream:
+ for event in stream:
+ if event.type == "content_block_delta":
+ if hasattr(event.delta, "text"):
+ yield event.delta.text
+ elif event.type == "tool_use":
+ yield f"\n[Tool requested: {event.name}]\n"
+
+ for chunk in stream_chunks():
+ self.current_full_response += chunk
+ yield chunk
+
+ async def cleanup(self):
+ """Clean up resources"""
+ await self.exit_stack.aclose()
+
+ async def process_query_with_openai_agent(
+ self, query: str, conversation_history=None
+ ):
+ if conversation_history:
+ messages = conversation_history.copy()
+ else:
+ messages = []
+ messages.append({"role": "user", "content": query})
+
+ async with MCPServerStdio(
+ name="StdioServer",
+ params=self.server_params,
+ client_session_timeout_seconds=300,
+ ) as server:
+ trace_id = gen_trace_id()
+ with trace(workflow_name="StdioServer", trace_id=trace_id):
+ print(
+ f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n"
+ )
+ agent = Agent(
+ name="Assistant",
+ instructions="Respond to the question to the best of abilities. Use the appropriate tools to solve the problem, if any.",
+ mcp_servers=[server],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
+ result = await Runner.run(starting_agent=agent, input=messages)
+ self.model_used = _openai_display_name
+
+ return result
diff --git a/src/server.py b/src/server.py
new file mode 100644
index 0000000..2674acc
--- /dev/null
+++ b/src/server.py
@@ -0,0 +1,421 @@
+import os
+import json
+from typing import Literal, Any
+
+import nats
+import asyncio
+from datetime import datetime
+from numpy import zeros
+
+from utils import ssl_config, NATS_SOURCE, NATS_PORT, MCP_PORT
+
+from mcp.server.fastmcp import FastMCP
+from litmussdk.devicehub import devices, tags
+from litmussdk.devicehub.drivers import driver_templates, DriverRecord
+from litmussdk.marketplace import list_all_containers, run_container
+from litmussdk.system import network, device_management
+from litmussdk.utils.env import update_env_variable
+
+
+# Create an MCP server
+mcp = FastMCP("LitmusMCPServer")
+mcp.settings.port = MCP_PORT
+
+
+@mcp.tool()
+def get_litmusedge_driver_list() -> list[str]:
+ """
+ Get List of Litmus Edge Drivers supported by Litmus MCP Server
+
+ Returns:
+ List of string driver names
+ """
+ list_drivers = [v for v in dir(driver_templates) if not v.startswith("_")]
+
+ return list_drivers
+
+
+@mcp.tool()
+def get_devicehub_devices() -> dict:
+ """
+ Retrieve all current DeviceHub devices configured on Litmus Edge.
+
+ Returns:
+ Dictionary of devices keyed by device name
+ """
+ output = {}
+ list_devices = devices.list_devices()
+ for current_device in list_devices:
+ output[current_device.name] = current_device.__dict__
+
+ return output
+
+
+@mcp.tool()
+def get_devicehub_device_tags(device_name: str) -> dict:
+ """
+ Retrieve all Tags of a single DeviceHub device configured on Litmus Edge.
+
+ Args:
+ device_name (str): Device name from where to grab tags
+
+ Returns:
+ Dictionary of tags keyed by tag name
+ """
+ output = {}
+ requested_device = None
+ list_devices = devices.list_devices()
+ for current_device in list_devices:
+ if device_name == current_device.name:
+ requested_device = current_device
+ if requested_device is None:
+ raise Exception(f"Device with name {device_name} not found")
+
+ list_tags = tags.list_registers_from_single_device(requested_device)
+ for current_tag in list_tags:
+ output[current_tag.tag_name] = current_tag.__dict__
+
+ return output
+
+
+@mcp.tool()
+def get_current_value_of_devicehub_tag(
+ device_name: str, tag_name: str | None, tag_id: str | None = None
+) -> dict | str:
+ """
+ Retrieve current value of a single DeviceHub device tag configured on Litmus Edge.
+
+ Args:
+ device_name:
+ tag_name: Tag name from where to grab tags
+ tag_id: (optional) Tag ID from where to grab tags
+
+ Returns:
+ Current value from raw topic
+ """
+ if not tag_name and not tag_id:
+ raise Exception("Either a Tag Name or Tag ID are required")
+
+ requested_device = None
+ list_devices = devices.list_devices()
+ for current_device in list_devices:
+ if device_name == current_device.name:
+ requested_device = current_device
+ if requested_device is None:
+ raise Exception(f"Device with name {device_name} not found")
+
+ list_tags = tags.list_registers_from_single_device(requested_device)
+ if tag_name:
+ requested_tag = next(
+ (tag for tag in list_tags if tag.tag_name == tag_name), None
+ )
+ else:
+ requested_tag = next((tag for tag in list_tags if tag.id == tag_id), None)
+
+ if requested_tag is None:
+ if tag_name:
+ raise Exception(f"Tag with name '{tag_name}' not found")
+ else:
+ raise Exception(f"Tag with ID '{tag_id}' not found")
+
+ requested_value_from_topic = next(
+ (topic.topic for topic in requested_tag.topics if topic.direction == "Output"),
+ "",
+ )
+ output = asyncio.run(get_current_value_on_topic(requested_value_from_topic))
+
+ return output
+
+
+@mcp.tool()
+def create_devicehub_device(
+ name: str,
+ selected_driver: str,
+):
+ """
+ Create a DeviceHub device on the connected Litmus Edge instance.
+
+ The DeviceHub module supports various protocols and manufacturers,
+ allowing register-based data polling via driver templates.
+
+ Args:
+ name: Name of the new device.
+ selected_driver: Driver template to use. Use `list_supported_drivers_on_edge` to view available options.
+ """
+ listed_driver = vars(driver_templates).get(selected_driver)
+ driver = DriverRecord.get(listed_driver.id)
+ properties = driver.get_default_properties()
+
+ device = devices.Device(name=name, properties=properties, driver=driver.id)
+ created_device = devices.create_device(device)
+ return created_device
+
+
+@mcp.tool()
+def update_environment_config(
+ key: Literal[
+ "EDGE_URL",
+ "EDGE_API_CLIENT_ID",
+ "EDGE_API_CLIENT_SECRET",
+ "VALIDATE_CERTIFICATE",
+ ],
+ value: str,
+) -> str:
+ """
+ Update Environment variables Config file for connecting to Litmus Edge
+
+ Args:
+ key (str): Keys such as EDGE_URL, EDGE_API_CLIENT_ID, EDGE_API_CLIENT_SECRET, VALIDATE_CERTIFICATE
+ value (str): Config value
+ """
+ update_env_variable(key=key, value=value)
+
+ return f"Config key {key} updated to {value}"
+
+
+@mcp.tool()
+def get_current_environment_config() -> dict:
+ """
+ Get the current environment configuration used for connecting to Litmus Edge.
+
+ Returns:
+ Dictionary of environment variable names and their values.
+ """
+ return {
+ "EDGE_URL": os.environ.get("EDGE_URL", ""),
+ "EDGE_API_CLIENT_ID": os.environ.get("EDGE_API_CLIENT_ID", ""),
+ "VALIDATE_CERTIFICATE": os.environ.get("VALIDATE_CERTIFICATE", ""),
+ }
+
+
+@mcp.tool()
+def get_litmusedge_friendly_name() -> str:
+ """
+ Get friendly name of LitmusEdge Device
+
+ Returns:
+ Device friendly name
+ """
+ return network.get_friendly_name()
+
+
+@mcp.tool()
+def set_litmusedge_friendly_name(new_friendly_name: str) -> None:
+ """
+ Change friendly name of LitmusEdge Device
+
+ Args:
+ new_friendly_name: New friendly name for the Device
+ """
+ return network.set_friendly_name(new_friendly_name)
+
+
+@mcp.tool()
+def get_cloud_activation_status() -> dict[str, Any]:
+ """
+ Get cloud activation status for the connection between Litmus Edge and Litmus Edge Manager
+
+ Returns:
+ Dictionary of cloud activation status
+ """
+ return device_management.show_cloud_registration_status()
+
+
+@mcp.tool()
+def get_all_containers_on_litmusedge() -> list[dict[str, Any]]:
+ """
+ List all containers in marketplace on Litmus Edge
+
+ Returns:
+ Array of all container/s details
+ """
+ return list_all_containers()
+
+
+@mcp.tool()
+def run_docker_container_on_litmusedge(docker_run_command: str) -> str:
+ """
+ Run a container in Litmus Edge marketplace with the docker command in the body.
+ This command runs on the litmus edge marketplace, not on the host system of the MCP server
+
+ Args:
+ docker_run_command: Docker run command to be run in string
+
+ Returns:
+ ID of container created
+ """
+ return run_container(docker_run_command)["id"]
+
+
+@mcp.tool()
+async def get_current_value_on_topic(
+ topic: str,
+ nats_source: str | None = None,
+ nats_port: str | None = None,
+) -> dict:
+ """
+ Subscribe to current value on a topic on Litmus Edge.
+ Change the global variable NATS_STATUS to "False" to end current subscription
+
+ Args:
+ topic: topic to subscribe to
+ nats_source: nats source, defaults to 10.30.50.1
+ nats_port: nats port, defaults to 4222
+
+ Returns:
+ Nats Subscription message
+ """
+ nats_source = nats_source or NATS_SOURCE
+ nats_port = nats_port or NATS_PORT
+
+ stop_event = asyncio.Event()
+
+ final_message = await nc_single_topic(nats_source, nats_port, topic, stop_event)
+ return final_message
+
+
+@mcp.tool()
+async def get_multiple_values_from_topic(
+ topic: str,
+ num_samples: int = 10,
+ nats_source: str | None = None,
+ nats_port: str | None = None,
+) -> object:
+ """
+ Get multiple values from a topic, for plotting or just returning a dictionary of value arrays
+
+ Args:
+ topic: NATS topic to subscribe to
+ num_samples: Number of messages to collect before plotting
+ nats_source: NATS source IP, defaults to 10.30.50.1
+ nats_port: NATS port, defaults to 4222
+
+ Returns:
+ Dictionary with timestamp and values as X and Y
+ """
+ nats_source = nats_source or NATS_SOURCE
+ nats_port = nats_port or NATS_PORT
+
+ stop_event = asyncio.Event()
+
+ output = await collect_multiple_values_from_topic(
+ nats_source, nats_port, topic, stop_event, num_samples
+ )
+
+ return output
+
+
+async def nc_single_topic(
+ nats_source: str,
+ nats_port: str,
+ nats_subscription_topic: str,
+ stop_event: asyncio.Event,
+) -> dict:
+ """
+ Subscribe to a single topic, and return a single message for the topic
+
+ Args:
+ nats_source: NATS source IP, defaults to 10.30.50.1
+ nats_port: NATS port, defaults to 4222
+ nats_subscription_topic: NATS topic to subscribe to
+ stop_event: Asyncio Event to stop collecting values
+
+ Returns:
+ Single message from the subscribed topic
+ """
+ ssl_context = ssl_config()
+ nc = await nats.connect(f"nats://{nats_source}:{nats_port}", tls=ssl_context)
+
+ result_message = {}
+
+ async def message_handler(msg):
+ nonlocal result_message
+ if result_message:
+ stop_event.set()
+
+ data = msg.data.decode()
+ message = json.loads(data)
+ result_message = message
+
+ await nc.subscribe(nats_subscription_topic, cb=message_handler)
+ await stop_event.wait()
+ await nc.drain()
+
+ return result_message
+
+
+async def collect_multiple_values_from_topic(
+ nats_source: str,
+ nats_port: str,
+ topic: str,
+ stop_event: asyncio.Event,
+ num_samples: int = 10,
+) -> object:
+ """
+ Collect multiple values from a topic, for plotting or just returning a dictionary
+
+ Args:
+ nats_source: NATS source IP, defaults to 10.30.50.1
+ nats_port: NATS port, defaults to 4222
+ topic: NATS topic to subscribe to
+ stop_event: Asyncio Event to stop collecting values
+ num_samples: Number of messages to collect
+
+ Returns:
+ Dict of results
+ """
+ ssl_context = ssl_config()
+ nc = await nats.connect(f"nats://{nats_source}:{nats_port}", tls=ssl_context)
+
+ results = {
+ # "timestamps": zeros(num_samples),
+ "humanTimestamps": ["" for _ in range(num_samples)],
+ "values": zeros(num_samples),
+ }
+ counter = 0
+
+ async def message_handler(msg):
+ nonlocal counter, results
+ data = msg.data.decode()
+ payload = json.loads(data)
+
+ value = payload["value"]
+ timestamp = payload["timestamp"]
+ human_ts = str(datetime.fromtimestamp(timestamp / 1000))
+
+ if counter < num_samples:
+ # results["timestamps"][counter] = timestamp
+ results["values"][counter] = value
+ results["humanTimestamps"][counter] = human_ts
+ counter += 1
+ else:
+ stop_event.set()
+ # # Sliding window
+ # if config["counter"] < num_samples:
+ # i = config["counter"]
+ # results["values"][i] = value
+ # results["timestamps"][i] = timestamp
+ # results["humanTimestamps"][i] = human_ts
+ # config["counter"] += 1
+ # else:
+ # config["counter"] = 0
+ # results["values"][:-1] = results["values"][1:]
+ # results["timestamps"][:-1] = results["timestamps"][1:]
+ # results["humanTimestamps"][:-1] = results["humanTimestamps"][1:]
+ #
+ # results["values"][-1] = value
+ # results["timestamps"][-1] = timestamp
+ # results["humanTimestamps"][-1] = human_ts
+ #
+ # if config["counter"] >= num_samples:
+ # stop_event.set()
+
+ await nc.subscribe(topic, cb=message_handler)
+ await stop_event.wait()
+ await nc.drain()
+
+ return results
+
+
+if __name__ == "__main__":
+ mcp.run(transport="stdio")
diff --git a/src/utils.py b/src/utils.py
new file mode 100644
index 0000000..09bcdff
--- /dev/null
+++ b/src/utils.py
@@ -0,0 +1,201 @@
+import ssl
+import os
+import logging
+from typing import List, Dict, Any, Tuple
+import dotenv
+
+NATS_SOURCE = "10.30.50.1"
+NATS_PORT = "4222"
+MCP_PORT = 8000
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+JINJA_TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
+STATIC_DIR = os.path.join(BASE_DIR, "static")
+
+
+EDGE_URL = ""
+EDGE_API_CLIENT_ID = ""
+EDGE_API_CLIENT_SECRET = ""
+VALIDATE_CERTIFICATE = ""
+ANTHROPIC_KEY = ""
+
+_key_EDGE_URL = "EDGE_URL"
+_key_EDGE_API_CLIENT_ID = "EDGE_API_CLIENT_ID"
+_key_EDGE_API_CLIENT_SECRET = "EDGE_API_CLIENT_SECRET"
+_key_VALIDATE_CERTIFICATE = "VALIDATE_CERTIFICATE"
+_default_value_VALIDATE_CERTIFICATE = "false"
+
+key_of_anthropic_api_key = "ANTHROPIC_API_KEY"
+key_of_openai_api_key = "OPENAI_API_KEY"
+MODEL_NAME_OPENAI = "openai"
+MODEL_NAME_ANTHROPIC = "anthropic"
+MODEL_PREFERENCE = "PREFERRED_MODEL"
+
+# Maximum number of message pairs to keep in history
+MAX_HISTORY_PAIRS = 5
+
+# Store conversation history in memory (this will be reset if the server restarts)
+CONVERSATION_HISTORY = []
+STREAMING_ALLOWED = True
+
+
+def ssl_config():
+ ssl_ctx = ssl.create_default_context()
+ ssl_ctx.check_hostname = False
+ ssl_ctx.verify_mode = ssl.CERT_NONE
+ return ssl_ctx
+
+
+def mcp_env_loader():
+ global EDGE_URL, EDGE_API_CLIENT_ID, EDGE_API_CLIENT_SECRET, VALIDATE_CERTIFICATE, ANTHROPIC_KEY
+
+ # Load .env file if available
+ dotenv_path = dotenv.find_dotenv()
+ if dotenv_path:
+ dotenv.load_dotenv(dotenv_path, override=True)
+ else:
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+ logging.info(
+ "Did not find .env file in current working directory. Defaulting to system variables"
+ )
+
+ EDGE_URL = os.environ.get(_key_EDGE_URL, "")
+ EDGE_API_CLIENT_ID = os.environ.get(_key_EDGE_API_CLIENT_ID, "")
+ EDGE_API_CLIENT_SECRET = os.environ.get(_key_EDGE_API_CLIENT_SECRET, "")
+ VALIDATE_CERTIFICATE = os.environ.get(
+ _key_VALIDATE_CERTIFICATE, _default_value_VALIDATE_CERTIFICATE
+ )
+ ANTHROPIC_KEY = os.environ.get(key_of_anthropic_api_key, "")
+
+
+def _get_env_vars(env_file, override):
+ path_to_env = dotenv.find_dotenv(env_file)
+ if path_to_env == "" or None:
+ with open(".env", "w") as f:
+ f.write("ENV=Initiate")
+ path_to_env = dotenv.find_dotenv(env_file)
+ dotenv.load_dotenv(path_to_env or env_file, override=override)
+ env_vars = {}
+
+ if os.path.exists(path_to_env):
+ with open(path_to_env, "r") as file:
+ for line in file:
+ if "=" in line:
+ k, v = line.strip().split("=", 1)
+ env_vars[k] = v
+ return env_vars, path_to_env
+
+
+def get_current_mcp_env(env_file: str = ".env"):
+ return _get_env_vars(env_file, override=False)
+
+
+def mcp_env_remover(key: str, env_file: str = ".env"):
+ env_vars, path_to_env = _get_env_vars(env_file, override=True)
+
+ env_vars.pop(key, None)
+ with open(path_to_env, "w") as file:
+ for k, v in env_vars.items():
+ file.write(f"{k}={v}\n")
+
+ print(f"Removed {key} in {env_file}")
+
+
+def mcp_env_updater(key: str, value: str | bool, env_file: str = ".env"):
+ env_vars, path_to_env = _get_env_vars(env_file, override=True)
+ env_vars[key] = value
+
+ # Write updated content back to .env
+ with open(path_to_env, "w") as file:
+ for k, v in env_vars.items():
+ file.write(f"{k}={v}\n")
+
+ print(f"Updated {key} in {env_file}")
+
+
+def check_model_key() -> Tuple[bool, str]:
+ anthropic_exists = os.environ.get(key_of_anthropic_api_key)
+ openai_exists = os.environ.get(key_of_openai_api_key)
+ preferred_model = os.environ.get(MODEL_PREFERENCE)
+
+ if preferred_model is not None and preferred_model in [
+ MODEL_NAME_OPENAI,
+ MODEL_NAME_ANTHROPIC,
+ ]:
+ return True, preferred_model
+
+ if anthropic_exists is None and openai_exists is None:
+ return False, ""
+
+ if openai_exists:
+ mcp_env_updater(MODEL_PREFERENCE, MODEL_NAME_OPENAI)
+ return True, MODEL_NAME_OPENAI
+
+ mcp_env_updater(MODEL_PREFERENCE, MODEL_NAME_ANTHROPIC)
+ return True, MODEL_NAME_ANTHROPIC
+
+
+def get_conversation_history() -> List[Dict[str, Any]]:
+ """Get global conversation history"""
+ global CONVERSATION_HISTORY
+ return CONVERSATION_HISTORY
+
+
+def update_conversation_history(query: str | None, response: str | None, clear=False):
+ """Update the global conversation history"""
+ global CONVERSATION_HISTORY
+ if clear:
+ CONVERSATION_HISTORY = []
+ return
+
+ # Add new messages
+ CONVERSATION_HISTORY.append({"role": "user", "content": query})
+ CONVERSATION_HISTORY.append({"role": "assistant", "content": response})
+
+ # Trim to keep only MAX_HISTORY_PAIRS
+ if len(CONVERSATION_HISTORY) > MAX_HISTORY_PAIRS * 2:
+ CONVERSATION_HISTORY = CONVERSATION_HISTORY[-MAX_HISTORY_PAIRS * 2 :]
+
+
+def get_chat_log(conversation_history) -> List[Dict[str, Any]]:
+ """Get chat log"""
+ chat_log = []
+
+ # Format conversation history for display
+ for i in range(0, len(conversation_history), 2):
+ if i + 1 < len(conversation_history):
+ chat_log.append(
+ {
+ "user": conversation_history[i]["content"],
+ "assistant": conversation_history[i + 1]["content"],
+ }
+ )
+ return chat_log
+
+
+def check_streaming_status(current_route: str) -> Tuple[bool, str]:
+ redirect = False
+ new_route = current_route
+
+ if STREAMING_ALLOWED and current_route == "/":
+ redirect = True
+ new_route = "/streaming"
+ return redirect, new_route
+ if not STREAMING_ALLOWED and current_route == "/streaming":
+ redirect = True
+ new_route = "/"
+ return redirect, new_route
+
+ return redirect, new_route
+
+
+def markdown_to_html(text):
+ if not text:
+ return ""
+ return text.replace("\n", " ")
+
+
+if __name__ == "__main__":
+ # env_loader()
+ print(BASE_DIR)
+ print(JINJA_TEMPLATE_DIR)
diff --git a/src/web_client.py b/src/web_client.py
new file mode 100644
index 0000000..2565330
--- /dev/null
+++ b/src/web_client.py
@@ -0,0 +1,346 @@
+import sys
+import logging
+import asyncio
+from typing import AsyncGenerator
+
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI, Request, Form, HTTPException
+from fastapi.exception_handlers import http_exception_handler
+from fastapi.responses import HTMLResponse
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.templating import Jinja2Templates
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import RedirectResponse, StreamingResponse, JSONResponse
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+from starlette.status import HTTP_303_SEE_OTHER
+import uvicorn
+
+from utils import (
+ mcp_env_loader,
+ mcp_env_updater,
+ key_of_anthropic_api_key,
+ get_current_mcp_env,
+ mcp_env_remover,
+ check_model_key,
+ get_conversation_history,
+ update_conversation_history,
+ get_chat_log,
+ check_streaming_status,
+ MODEL_NAME_ANTHROPIC,
+ key_of_openai_api_key,
+ MODEL_PREFERENCE,
+ MODEL_NAME_OPENAI,
+ markdown_to_html,
+)
+from utils import JINJA_TEMPLATE_DIR, STATIC_DIR
+from client_utils import MCPClient
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger("web_client")
+
+SERVER_SCRIPT = sys.argv[1] if len(sys.argv) > 1 else "server.py"
+mcp_env_loader()
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ logger.info("Starting MCP client connection")
+ client = MCPClient()
+ await client.connect_to_server(SERVER_SCRIPT)
+ app.state.client = client # type: ignore[arg-type]
+
+ yield
+ logger.info("Cleaning up MCP client")
+ await client.cleanup()
+
+ logger.info("MCP client cleaned up")
+
+
+app = FastAPI(lifespan=lifespan)
+templates = Jinja2Templates(directory=JINJA_TEMPLATE_DIR)
+templates.env.filters["markdown_to_html"] = markdown_to_html
+app.mount(STATIC_DIR, StaticFiles(directory=STATIC_DIR), name="static")
+app.add_middleware(
+ CORSMiddleware, # type: ignore[arg-type]
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.api_route(
+ "/setup", methods=["GET", "POST"], response_class=HTMLResponse, name="setup"
+)
+async def setup(request: Request):
+ if request.method == "GET":
+ return templates.TemplateResponse("setup.html", {"request": request})
+
+ form = await request.form()
+ value_of_anthropic_api_key = form.get("value_of_anthropic_api_key", "").strip()
+ value_of_openai_api_key = form.get("value_of_openai_api_key", "").strip()
+ if not value_of_anthropic_api_key and not value_of_openai_api_key:
+ return templates.TemplateResponse(
+ "setup.html",
+ {
+ "request": request,
+ "error": "Please provide at least one API key.",
+ },
+ )
+ if value_of_anthropic_api_key:
+ mcp_env_updater(key_of_anthropic_api_key, value_of_anthropic_api_key)
+ if value_of_openai_api_key:
+ mcp_env_updater(key_of_openai_api_key, value_of_openai_api_key)
+
+ return RedirectResponse("/", status_code=HTTP_303_SEE_OTHER)
+
+
+@app.post("/switch-model", response_class=HTMLResponse, name="switch_model")
+async def switch_model(request: Request, switch_model_to: str = Form(...)):
+ if switch_model_to.startswith("anthropic"):
+ preference = MODEL_NAME_ANTHROPIC
+ else:
+ preference = MODEL_NAME_OPENAI
+ mcp_env_updater(MODEL_PREFERENCE, preference)
+ mcp_env_loader()
+ return RedirectResponse("/update-env?updated=true", status_code=HTTP_303_SEE_OTHER)
+
+
+@app.get("/update-env", response_class=HTMLResponse, name="update_env_form")
+async def update_env_form(request: Request):
+ current_env, _ = get_current_mcp_env()
+ _, model_type = check_model_key()
+ updated = request.query_params.get("updated") == "true"
+ return templates.TemplateResponse(
+ "update_env.html",
+ {
+ "request": request,
+ "env": current_env,
+ "updated": updated,
+ "current_model": model_type,
+ },
+ )
+
+
+@app.post("/update-env", response_class=HTMLResponse)
+async def update_env_submit(
+ request: Request, env_key: str = Form(...), env_value: str = Form(...)
+):
+ mcp_env_updater(env_key, env_value)
+ mcp_env_loader()
+ return RedirectResponse("/update-env?updated=true", status_code=HTTP_303_SEE_OTHER)
+
+
+@app.post("/remove-env", response_class=HTMLResponse, name="remove_env")
+async def remove_env_submit(request: Request, env_key: str = Form(...)):
+ mcp_env_remover(key=env_key)
+ mcp_env_loader()
+ return RedirectResponse("/update-env?updated=true", status_code=HTTP_303_SEE_OTHER)
+
+
+@app.post("/clear-history", response_class=HTMLResponse, name="clear_history")
+async def clear_history(request: Request):
+ # Clear the conversation history through the util function
+ update_conversation_history(None, None, clear=True)
+ return RedirectResponse("/", status_code=HTTP_303_SEE_OTHER)
+
+
+@app.api_route(
+ "/", methods=["GET", "POST"], response_class=HTMLResponse, name="query_handler"
+)
+async def query_handler(request: Request):
+ ## If Stream-only mode
+ # redirect_flag, redirect_url = check_streaming_status(current_route=request.url.path)
+ # if redirect_flag:
+ # return RedirectResponse(redirect_url, status_code=HTTP_303_SEE_OTHER)
+
+ mcp_env_loader()
+ client = getattr(request.app.state, "client", None)
+ if not client:
+ logger.error("MCP client not initialized")
+ raise HTTPException(
+ status_code=500, detail="Internal server error: MCP client not initialized"
+ )
+ key_exists, model_type = check_model_key()
+ if not key_exists:
+ logger.info("Model key not found, redirecting to setup")
+ return RedirectResponse("/setup", status_code=HTTP_303_SEE_OTHER)
+
+ # Get conversation history
+ conversation_history = get_conversation_history()
+ chat_log = get_chat_log(conversation_history)
+
+ if request.method == "POST":
+ form = await request.form()
+ query = form.get("query", "").strip()
+
+ # Process query with conversation history
+ if model_type == MODEL_NAME_ANTHROPIC:
+ response_text = await client.process_query_anthropic(
+ query, conversation_history=conversation_history
+ )
+ else:
+ full_response_text = await client.process_query_with_openai_agent(
+ query, conversation_history=conversation_history
+ )
+ response_text = full_response_text.final_output
+
+ # Update conversation history
+ update_conversation_history(query, response_text)
+
+ # Add current exchange to chat log for display
+ chat_log.append({"user": query, "assistant": response_text})
+ else:
+ query = ""
+ response_text = ""
+
+ return templates.TemplateResponse(
+ "query.html",
+ {
+ "request": request,
+ "query": query,
+ "response_text": response_text,
+ "chat_log": [
+ {
+ "user": entry.get("user", ""),
+ "assistant": entry.get("assistant", ""),
+ "model": entry.get("model", client.model_used or model_type),
+ }
+ for entry in chat_log
+ ],
+ "has_history": len(chat_log) > 0,
+ },
+ )
+
+
+## Streaming
+@app.api_route("/streaming", methods=["GET", "POST"], name="streaming_query_handler")
+async def streaming_query_handler(request: Request):
+ redirect_flag, redirect_url = check_streaming_status(current_route=request.url.path)
+ if redirect_flag:
+ return RedirectResponse(redirect_url, status_code=HTTP_303_SEE_OTHER)
+
+ mcp_env_loader()
+ client = getattr(request.app.state, "client", None)
+ if not client:
+ logger.error("MCP client not initialized")
+ if request.method == "POST":
+ return JSONResponse(
+ status_code=500,
+ content={"error": "Internal server error: MCP client not initialized"},
+ )
+ raise HTTPException(
+ status_code=500, detail="Internal server error: MCP client not initialized"
+ )
+ key_exists, model_type = check_model_key()
+ ## TODO remove anthropic enforcement
+ if not key_exists and model_type != MODEL_NAME_ANTHROPIC:
+ logger.info("Model key not found, redirecting to setup")
+ return RedirectResponse("/setup", status_code=HTTP_303_SEE_OTHER)
+
+ # Get conversation history
+ conversation_history = get_conversation_history()
+ chat_log = get_chat_log(conversation_history)
+
+ if request.method == "POST":
+ try:
+ form = await request.form()
+ query = form.get("query", "").strip()
+
+ if not query:
+ return JSONResponse(
+ status_code=400, content={"error": "Query cannot be empty"}
+ )
+
+ async def stream_response() -> AsyncGenerator[str, None]:
+ full_response = ""
+ try:
+ # Set up SSE-style headers
+ yield "Content-Type: text/event-stream\r\n"
+ yield "Cache-Control: no-cache\r\n"
+ yield "\r\n"
+
+ async for chunk in client.process_streaming_query(
+ query, conversation_history=conversation_history
+ ):
+ if chunk: # Only process non-empty chunks
+ full_response += chunk
+ yield chunk
+ # Add a small delay to prevent overwhelming the browser
+ await asyncio.sleep(0.01)
+
+ update_conversation_history(query, full_response)
+ except Exception as e:
+ logger.exception(f"Error in streaming response: {str(e)}")
+ error_msg = f"Error: {str(e)}"
+ yield error_msg
+ if hasattr(client, "current_full_response"):
+ client.current_full_response += error_msg
+
+ return StreamingResponse(
+ stream_response(),
+ media_type="text/plain",
+ headers={
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ "Pragma": "no-cache",
+ "Expires": "0",
+ },
+ )
+ except Exception as e:
+ logger.exception(f"Error setting up streaming: {str(e)}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": f"Error setting up streaming: {str(e)}"},
+ )
+
+ return templates.TemplateResponse(
+ "query_streaming.html",
+ {
+ "request": request,
+ "chat_log": [
+ {
+ "user": entry.get("user", ""),
+ "assistant": entry.get("assistant", ""),
+ }
+ for entry in chat_log
+ ],
+ "model": client.model_used or model_type,
+ "has_history": len(chat_log) > 0,
+ },
+ )
+
+
+@app.get("/health", response_class=HTMLResponse, name="health")
+async def health_check(request: Request):
+ return templates.TemplateResponse(
+ "health.html", {"request": request, "status": "ok", "version": "1.0"}
+ )
+
+
+@app.exception_handler(StarletteHTTPException)
+async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException):
+ if exc.status_code == 404:
+ return RedirectResponse(url="/")
+ elif exc.status_code == 500:
+ return templates.TemplateResponse(
+ "500.html", {"request": request}, status_code=500
+ )
+ return await http_exception_handler(request, exc)
+
+
+if __name__ == "__main__":
+ import webbrowser
+
+ try:
+ webbrowser.open("http://localhost:9000")
+ uvicorn.run("web_client:app", host="0.0.0.0", port=9000, reload=True)
+ except KeyboardInterrupt:
+ logger.info("Server shutdown requested")
+ except Exception as e:
+ logger.exception(f"Error running server: {str(e)}")
diff --git a/static/MCP-server-arch-diagram.png b/static/MCP-server-arch-diagram.png
new file mode 100644
index 0000000..83b90ed
Binary files /dev/null and b/static/MCP-server-arch-diagram.png differ
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..9d7c3ec
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/litmus-logo-dark.svg b/static/litmus-logo-dark.svg
new file mode 100644
index 0000000..d350151
--- /dev/null
+++ b/static/litmus-logo-dark.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/static/litmus-logo-light.svg b/static/litmus-logo-light.svg
new file mode 100644
index 0000000..b650f6f
--- /dev/null
+++ b/static/litmus-logo-light.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/static/query_script.js b/static/query_script.js
new file mode 100644
index 0000000..6c9a064
--- /dev/null
+++ b/static/query_script.js
@@ -0,0 +1,59 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const form = document.getElementById("query-form");
+ const textarea = document.getElementById("query");
+ const loading = document.getElementById("loading");
+ const submitButton = form?.querySelector("button[type='submit']");
+ const clearHistoryForm = document.getElementById("clear-history-form");
+
+ // Add a confirmation for clearing history
+ if (clearHistoryForm) {
+ clearHistoryForm.addEventListener("submit", function (e) {
+ if (!confirm("Are you sure you want to clear all chat history?")) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ if (!form || !textarea || !loading || !submitButton) {
+ console.warn("Essential elements for non-streaming form not found");
+ return;
+ }
+
+ // Auto-resize textarea
+ function resizeTextarea() {
+ textarea.style.height = "auto";
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ }
+
+ resizeTextarea();
+ textarea.addEventListener("input", resizeTextarea);
+
+ let isSubmitting = false;
+
+ function handlePreSubmit() {
+ const query = textarea.value.trim();
+ if (!query || isSubmitting) return false;
+
+ isSubmitting = true;
+ submitButton.disabled = true; // ✅ Disable only button
+ loading.style.display = "flex";
+ return true;
+ }
+
+ // Handle Enter key submission
+ textarea.addEventListener("keydown", function (e) {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ if (handlePreSubmit()) {
+ form.requestSubmit(); // Will POST and reload
+ }
+ }
+ });
+
+ // Handle manual submit click
+ form.addEventListener("submit", function () {
+ if (!isSubmitting) {
+ handlePreSubmit();
+ }
+ });
+});
diff --git a/static/query_streaming_script.js b/static/query_streaming_script.js
new file mode 100644
index 0000000..bd83fcb
--- /dev/null
+++ b/static/query_streaming_script.js
@@ -0,0 +1,164 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const form = document.getElementById("query-form");
+ if (!form) return;
+
+ const textarea = document.getElementById("query");
+ const loading = document.getElementById("loading");
+ const userQuery = document.getElementById("user-query");
+ const responseStream = document.getElementById("response-stream");
+ const currentQueryContainer = document.getElementById("current-query-container");
+ const currentResponseContainer = document.getElementById("current-response-container");
+ const submitButton = form.querySelector("button[type='submit']");
+ const clearHistoryForm = document.getElementById("clear-stream-history-form");
+
+ // Add a confirmation for clearing history
+ if (clearHistoryForm) {
+ clearHistoryForm.addEventListener("submit", function(e) {
+ if (!confirm("Are you sure you want to clear all chat history?")) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ if (!textarea || !loading || !userQuery || !responseStream ||
+ !currentQueryContainer || !currentResponseContainer || !submitButton) {
+ console.error("Required streaming mode elements not found");
+ return;
+ }
+
+ let isSubmitting = false;
+
+ function resizeTextarea() {
+ textarea.style.height = "auto";
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ }
+
+ resizeTextarea();
+ textarea.addEventListener("input", resizeTextarea);
+
+ let cursorInterval;
+
+ function stopCursor() {
+ if (cursorInterval) {
+ clearInterval(cursorInterval);
+ cursorInterval = null;
+ if (responseStream.textContent.endsWith("▍")) {
+ responseStream.textContent = responseStream.textContent.slice(0, -1);
+ }
+ }
+ }
+
+ function startCursor() {
+ stopCursor();
+ cursorInterval = setInterval(() => {
+ if (responseStream.textContent.endsWith("▍")) {
+ responseStream.textContent = responseStream.textContent.slice(0, -1);
+ } else {
+ responseStream.textContent += "▍";
+ }
+ window.scrollTo(0, document.body.scrollHeight);
+ }, 500);
+ }
+
+ async function submitQuery() {
+ const query = textarea.value.trim();
+ if (!query || isSubmitting) return;
+
+ try {
+ isSubmitting = true;
+
+ loading.style.display = "flex";
+ textarea.disabled = true;
+ submitButton.disabled = true;
+
+ userQuery.textContent = query;
+ responseStream.textContent = "";
+ currentQueryContainer.style.display = "block";
+ currentResponseContainer.style.display = "block";
+
+ const formData = new FormData();
+ formData.append("query", query);
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", "/streaming", true);
+
+ let responseText = "";
+ let lastProcessedLength = 0;
+
+ xhr.onprogress = function () {
+ if (xhr.status === 200) {
+ const newChunk = xhr.responseText.substring(lastProcessedLength);
+ lastProcessedLength = xhr.responseText.length;
+ if (newChunk) {
+ stopCursor();
+ responseStream.textContent += newChunk;
+ startCursor();
+ responseText += newChunk;
+ window.scrollTo(0, document.body.scrollHeight);
+ }
+ }
+ };
+
+ xhr.onload = function () {
+ if (xhr.status === 200) {
+ const finalChunk = xhr.responseText.substring(lastProcessedLength);
+ if (finalChunk) {
+ stopCursor();
+ responseStream.textContent += finalChunk;
+ }
+ } else {
+ console.error("Request failed with status:", xhr.status);
+ responseStream.textContent = `Error ${xhr.status}: Request failed`;
+ }
+ finishSubmission();
+ };
+
+ xhr.onerror = function () {
+ console.error("Network error occurred");
+ responseStream.textContent = "Network error occurred. Please try again.";
+ finishSubmission();
+ };
+
+ xhr.ontimeout = function () {
+ console.error("Request timed out");
+ responseStream.textContent = "Request timed out. Please try again.";
+ finishSubmission();
+ };
+
+ xhr.send(formData);
+ startCursor();
+
+ } catch (error) {
+ console.error("Error submitting query:", error);
+ responseStream.textContent = `Error: ${error.message}`;
+ finishSubmission();
+ }
+ }
+
+ function finishSubmission() {
+ stopCursor();
+ loading.style.display = "none";
+ textarea.disabled = false;
+ submitButton.disabled = false;
+ textarea.value = "";
+ resizeTextarea();
+ textarea.focus();
+ isSubmitting = false;
+ window.scrollTo(0, document.body.scrollHeight);
+ }
+
+ form.addEventListener("submit", function (e) {
+ e.preventDefault();
+ submitQuery();
+ });
+
+ textarea.addEventListener("keydown", function (e) {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ submitQuery();
+ } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ submitQuery();
+ }
+ });
+});
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..3bda627
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,360 @@
+hr {
+ border: 0;
+ height: 1px;
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
+}
+h1 {
+ margin-bottom: 24px;
+ font-weight: 600;
+}
+
+body {
+ background-color: #f2f8f6;
+ font-family: 'Space Grotesk', sans-serif;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ min-width: 700px;
+ color: #002f2b;
+}
+/* Top nav*/
+.top-nav {
+ width: 100%;
+ display: inline-flex;
+ background: transparent;
+ overflow: hidden;
+}
+.top-nav a{
+ color: #002f2b;
+ border-radius: 8px;
+ font-size: 16px;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
+ cursor: pointer;
+ padding: 8px 10px;
+}
+.top-nav a:hover {
+ background-color: #e6fbf3;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+}
+.top-nav a:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(61, 219, 144, 0.2);
+}
+
+/*Body Container*/
+.container {
+ background: white;
+ padding: 40px;
+ border-radius: 12px;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+ width: 100%;
+ max-width: 700px;
+ box-sizing: border-box;
+ text-align: left;
+}
+
+/*Input form field*/
+input[type="text"] {
+ width: 100%;
+ padding: 14px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-sizing: border-box;
+ font-family: 'Helvetica', 'Arial', sans-serif;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
+}
+textarea[name="query"] {
+ width: 100%;
+ min-height: 44px;
+ padding: 14px;
+ font-size: 16px;
+ font-family: 'Helvetica', 'Arial', sans-serif;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-sizing: border-box;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
+ resize: none;
+ overflow-y: hidden;
+ line-height: 1.4;
+ transition: height 0.15s ease-in-out;
+}
+textarea + small {
+ text-align: right;
+ display: block;
+ margin-top: -14px;
+ margin-bottom: 16px;
+}
+
+
+/*Button*/
+button {
+ background-color: #4fb896;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ font-size: 16px;
+ font-family: 'Space Grotesk', sans-serif;
+ font-weight: 500;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+ height: fit-content;
+}
+button:hover {
+ background-color: #70c7a9;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+button:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(79, 184, 150, 0.3);
+}
+
+.action-buttons {
+ display: flex;
+ gap: 10px;
+}
+.side-button {
+ background-color: #f2f8f6;
+ color: #002f2b;
+ font-size: 14px;
+ padding: 8px 12px;
+ border-radius: 8px;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #dce8e2;
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
+ cursor: pointer;
+}
+.side-button:hover {
+ background-color: #e6fbf3;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+}
+.side-button:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(61, 219, 144, 0.2);
+}
+select.select-button {
+ background-color: #f2f8f6;
+ color: #002f2b;
+ font-size: 14px;
+ padding: 8px 12px;
+ border-radius: 8px;
+ border: 1px solid #dce8e2;
+ font-family: 'Space Grotesk', sans-serif;
+ cursor: pointer;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
+ height: 40px;
+ line-height: 1.4;
+}
+select.select-button:hover {
+ background-color: #e6fbf3;
+}
+select.select-button:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(61, 219, 144, 0.2);
+}
+
+
+.md-icon-btn {
+ background-color: transparent;
+ padding: 6px;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s ease;
+ width: 40px;
+ height: 40px;
+ font-size: 0;
+}
+.md-icon-btn svg {
+ fill: #e04848;
+ transition: fill 0.2s ease;
+ width: 20px;
+ height: 20px;
+}
+.md-icon-btn:hover {
+ background-color: rgba(224, 72, 72, 0.1);
+}
+.md-icon-btn:hover svg {
+ fill: #b43d3d;
+}
+.md-icon-btn:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(224, 72, 72, 0.3);
+}
+
+/*Loading animation and text*/
+.loading {
+ display: none;
+ align-items: center;
+ font-style: normal;
+ color: #278a6b;
+ font-weight: 500;
+ font-size: 18px;
+}
+
+.loading .dot-pulse {
+ display: flex;
+ gap: 4px;
+ margin-right: 8px;
+}
+
+.loading .dot-pulse div {
+ width: 10px;
+ height: 10px;
+ background-color: #278a6b;
+ border-radius: 50%;
+ animation: pulse 0.9s infinite ease-in-out;
+ opacity: 1;
+ transform: scale(1);
+}
+
+.loading .dot-pulse div:nth-child(2) {
+ animation-delay: 0.15s;
+}
+
+.loading .dot-pulse div:nth-child(3) {
+ animation-delay: 0.3s;
+}
+
+@keyframes pulse {
+ 0%, 80%, 100% {
+ transform: scale(0.95);
+ opacity: 0.85;
+ }
+ 40% {
+ transform: scale(1.4);
+ opacity: 1;
+ }
+}
+
+@keyframes pulse {
+ 0%, 80%, 100% {
+ transform: scale(0.9);
+ opacity: 0.6;
+ }
+ 40% {
+ transform: scale(1.2);
+ opacity: 1;
+ }
+}
+
+/*Response part*/
+.query-label {
+ font-weight: 600;
+ margin-top: 30px;
+}
+
+.response {
+ margin-top: 12px;
+ background: #f2f8f6;
+ padding: 20px;
+ border-radius: 8px;
+ border: 1px solid #dce8e2;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/*Chat from user and mcp response*/
+.chat-log {
+ margin-top: 30px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.chat-message {
+ padding: 16px;
+ border-radius: 12px;
+ box-sizing: border-box;
+}
+
+.chat-message.user {
+ background-color: #edfdf6;
+ border: 1px solid #88eabb;
+ color: #002f2b;
+}
+
+.chat-message.mcp {
+ background-color: #f1f0ff;
+ border: 1px solid #c2c1fb;
+ color: #032125;
+}
+
+.chat-message .label {
+ font-weight: 600;
+ color: #002f2b;
+ margin-bottom: 6px;
+}
+
+.chat-message .message {
+ white-space: pre-wrap;
+ word-break: break-word;
+ line-height: 1.6;
+}
+
+.form-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 10px;
+}
+
+/*Update Config*/
+.toast {
+ background-color: #e6fbf3;
+ border: 1px solid #3adb90;
+ color: #002f2b;
+ padding: 10px 14px;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 6px;
+ margin-bottom: 16px;
+ opacity: 1;
+ transition: opacity 0.6s ease;
+}
+
+/*Update config and get env variables*/
+.env-form {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+.env-form button{
+ margin-bottom: auto;
+}
+.env-table {
+ width: 100%;
+ max-width: 100%;
+ table-layout: fixed;
+ border-collapse: collapse;
+ font-size: 14px;
+ word-break: break-word;
+}
+.env-table th,
+.env-table td {
+ padding: 8px 12px;
+ border-bottom: 1px solid #e0e0e0;
+ text-align: left;
+}
+
+.env-table th {
+ font-weight: 600;
+ background-color: #f2f8f6;
+}
\ No newline at end of file
diff --git a/templates/500.html b/templates/500.html
new file mode 100644
index 0000000..38a5fa7
--- /dev/null
+++ b/templates/500.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% block scripts %}
+
+{% endblock %}
+
+{% block title %}Server Error · Litmus MCP Server{% endblock %}
+
+{% block content %}
+
+
Litmus MCP Server 🧠
+
+
+
+
500 - Internal Server Error
+
Oops! Something went wrong on our end, or by the API call timeouts.
+
Home
+
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..fdf8d53
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,23 @@
+
+
+
+
+ {% block title %}Litmus MCP Server 🧠{% endblock %}
+
+
+
+ {% block scripts %}{% endblock %}
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
diff --git a/templates/health.html b/templates/health.html
new file mode 100644
index 0000000..13cdf18
--- /dev/null
+++ b/templates/health.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% block title %}Health Check · Litmus MCP Server{% endblock %}
+
+{% block content %}
+
+
Litmus MCP Server 🧠
+
+
+
+
+
+
Health Status 🩺
+
Status: {{ status }}
+
Version: {{ version }}
+
+
+
+{% endblock %}
diff --git a/templates/query.html b/templates/query.html
new file mode 100644
index 0000000..096b95b
--- /dev/null
+++ b/templates/query.html
@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+{% block scripts %}
+
+{% endblock %}
+{% block title %}Query · Litmus MCP Server{% endblock %}
+
+{% block content %}
+
+
Litmus MCP Server 🧠
+
+ {% if has_history %}
+
+ {% endif %}
+
+
+
+
+
+{% if chat_log %}
+
+ {% for exchange in chat_log | reverse %} {# Reverse chat #}
+
+
You:
+
{{ exchange.user | markdown_to_html | safe }}
+
+
+
MCP Server: (Model: {{ exchange.model | safe }})
+
{{ exchange.assistant | markdown_to_html | safe }}
+
+ {% endfor %}
+
+{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/query_streaming.html b/templates/query_streaming.html
new file mode 100644
index 0000000..fc702c4
--- /dev/null
+++ b/templates/query_streaming.html
@@ -0,0 +1,74 @@
+{% extends "base.html" %}
+
+{% block title %}Query · Litmus MCP Server{% endblock %}
+
+{% block content %}
+
+
Litmus MCP Server 🧠
+
+ {% if has_history %}
+
+ 🧹 Clear History
+
+ {% endif %}
+
+
+
+
+ ⚠️ Streaming response is currently in beta , and only supported with Anthropic Models .
+
+
+
+
+
+ Press Enter to send, Shift +Enter for newline
+
+
+
+
+
+ {% for exchange in chat_log %}
+
+
You:
+
{{ exchange.user | safe }}
+
+
+
MCP Server: (Model: {{ exchange.model | safe }})
+
{{ exchange.assistant | safe }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/setup.html b/templates/setup.html
new file mode 100644
index 0000000..651b636
--- /dev/null
+++ b/templates/setup.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+
+{% block title %}Setup · Litmus MCP Server{% endblock %}
+
+{% block content %}
+Setup Litmus MCP Server 🔑
+Please enter either your Anthropic API key, and/or OPEN AI API Key to start/update the server.
+
+
+
+ Anthropic API Key:
+
+
+ OPEN AI API Key:
+
+
+ Load MCP Server
+ {% if error %}
+ {{ error }}
+ {% endif %}
+
+
+{% endblock %}
+
diff --git a/templates/update_env.html b/templates/update_env.html
new file mode 100644
index 0000000..c96ec45
--- /dev/null
+++ b/templates/update_env.html
@@ -0,0 +1,81 @@
+{% extends "base.html" %}
+
+{% block title %}Config · Litmus MCP Server{% endblock %}
+
+{% block content %}
+
+
Update Config Variables ⚙️
+
+
+
+
+
+
+
+
+ 🤖 Current Model: {{ current_model | safe }}
+ Open AI GPT-4o
+ Anthropic Claude 3.7 Sonnet
+
+
+
+
+
+
+
+ Update
+
+
+{% if updated %}
+✅ Environment variable updated.
+
+{% endif %}
+
+
+
+Current Environment
+
+
+
+
+
+
+
+
+ Key
+ Value
+
+
+
+
+ {% for key, value in env.items() %}
+
+ {{ key }}
+ {{ value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..3fc07aa
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,977 @@
+version = 1
+revision = 1
+requires-python = ">=3.12"
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anthropic"
+version = "0.49.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+]
+
+[[package]]
+name = "black"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
+ { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
+ { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
+ { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
+ { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
+ { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
+ { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
+ { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
+ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.115.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
+]
+
+[[package]]
+name = "griffe"
+version = "1.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303 },
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
+]
+
+[[package]]
+name = "jiter"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/d7/c55086103d6f29b694ec79156242304adf521577530d9031317ce5338c59/jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11", size = 309203 },
+ { url = "https://files.pythonhosted.org/packages/b0/01/f775dfee50beb420adfd6baf58d1c4d437de41c9b666ddf127c065e5a488/jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e", size = 319678 },
+ { url = "https://files.pythonhosted.org/packages/ab/b8/09b73a793714726893e5d46d5c534a63709261af3d24444ad07885ce87cb/jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2", size = 341816 },
+ { url = "https://files.pythonhosted.org/packages/35/6f/b8f89ec5398b2b0d344257138182cc090302854ed63ed9c9051e9c673441/jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75", size = 364152 },
+ { url = "https://files.pythonhosted.org/packages/9b/ca/978cc3183113b8e4484cc7e210a9ad3c6614396e7abd5407ea8aa1458eef/jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d", size = 406991 },
+ { url = "https://files.pythonhosted.org/packages/13/3a/72861883e11a36d6aa314b4922125f6ae90bdccc225cd96d24cc78a66385/jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42", size = 395824 },
+ { url = "https://files.pythonhosted.org/packages/87/67/22728a86ef53589c3720225778f7c5fdb617080e3deaed58b04789418212/jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc", size = 351318 },
+ { url = "https://files.pythonhosted.org/packages/69/b9/f39728e2e2007276806d7a6609cda7fac44ffa28ca0d02c49a4f397cc0d9/jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc", size = 384591 },
+ { url = "https://files.pythonhosted.org/packages/eb/8f/8a708bc7fd87b8a5d861f1c118a995eccbe6d672fe10c9753e67362d0dd0/jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e", size = 520746 },
+ { url = "https://files.pythonhosted.org/packages/95/1e/65680c7488bd2365dbd2980adaf63c562d3d41d3faac192ebc7ef5b4ae25/jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d", size = 512754 },
+ { url = "https://files.pythonhosted.org/packages/78/f3/fdc43547a9ee6e93c837685da704fb6da7dba311fc022e2766d5277dfde5/jiter-0.9.0-cp312-cp312-win32.whl", hash = "sha256:699edfde481e191d81f9cf6d2211debbfe4bd92f06410e7637dffb8dd5dfde06", size = 207075 },
+ { url = "https://files.pythonhosted.org/packages/cd/9d/742b289016d155f49028fe1bfbeb935c9bf0ffeefdf77daf4a63a42bb72b/jiter-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:099500d07b43f61d8bd780466d429c45a7b25411b334c60ca875fa775f68ccb0", size = 207999 },
+ { url = "https://files.pythonhosted.org/packages/e7/1b/4cd165c362e8f2f520fdb43245e2b414f42a255921248b4f8b9c8d871ff1/jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7", size = 308197 },
+ { url = "https://files.pythonhosted.org/packages/13/aa/7a890dfe29c84c9a82064a9fe36079c7c0309c91b70c380dc138f9bea44a/jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b", size = 318160 },
+ { url = "https://files.pythonhosted.org/packages/6a/38/5888b43fc01102f733f085673c4f0be5a298f69808ec63de55051754e390/jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69", size = 341259 },
+ { url = "https://files.pythonhosted.org/packages/3d/5e/bbdbb63305bcc01006de683b6228cd061458b9b7bb9b8d9bc348a58e5dc2/jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103", size = 363730 },
+ { url = "https://files.pythonhosted.org/packages/75/85/53a3edc616992fe4af6814c25f91ee3b1e22f7678e979b6ea82d3bc0667e/jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635", size = 405126 },
+ { url = "https://files.pythonhosted.org/packages/ae/b3/1ee26b12b2693bd3f0b71d3188e4e5d817b12e3c630a09e099e0a89e28fa/jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4", size = 393668 },
+ { url = "https://files.pythonhosted.org/packages/11/87/e084ce261950c1861773ab534d49127d1517b629478304d328493f980791/jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d", size = 352350 },
+ { url = "https://files.pythonhosted.org/packages/f0/06/7dca84b04987e9df563610aa0bc154ea176e50358af532ab40ffb87434df/jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3", size = 384204 },
+ { url = "https://files.pythonhosted.org/packages/16/2f/82e1c6020db72f397dd070eec0c85ebc4df7c88967bc86d3ce9864148f28/jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5", size = 520322 },
+ { url = "https://files.pythonhosted.org/packages/36/fd/4f0cd3abe83ce208991ca61e7e5df915aa35b67f1c0633eb7cf2f2e88ec7/jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d", size = 512184 },
+ { url = "https://files.pythonhosted.org/packages/a0/3c/8a56f6d547731a0b4410a2d9d16bf39c861046f91f57c98f7cab3d2aa9ce/jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53", size = 206504 },
+ { url = "https://files.pythonhosted.org/packages/f4/1c/0c996fd90639acda75ed7fa698ee5fd7d80243057185dc2f63d4c1c9f6b9/jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7", size = 204943 },
+ { url = "https://files.pythonhosted.org/packages/78/0f/77a63ca7aa5fed9a1b9135af57e190d905bcd3702b36aca46a01090d39ad/jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001", size = 317281 },
+ { url = "https://files.pythonhosted.org/packages/f9/39/a3a1571712c2bf6ec4c657f0d66da114a63a2e32b7e4eb8e0b83295ee034/jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a", size = 350273 },
+ { url = "https://files.pythonhosted.org/packages/ee/47/3729f00f35a696e68da15d64eb9283c330e776f3b5789bac7f2c0c4df209/jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf", size = 206867 },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.23.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 },
+]
+
+[[package]]
+name = "litmus-mcp-server"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "fastapi" },
+ { name = "jinja2" },
+ { name = "litmussdk" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "nats-py" },
+ { name = "numpy" },
+ { name = "python-multipart" },
+]
+
+[package.dev-dependencies]
+lint = [
+ { name = "black" },
+ { name = "radon" },
+ { name = "ruff" },
+]
+llm-sdks = [
+ { name = "anthropic" },
+ { name = "openai-agents" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastapi", specifier = ">=0.115.12" },
+ { name = "jinja2", specifier = ">=3.1.6" },
+ { name = "litmussdk", url = "https://github.com/litmusautomation/litmus-sdk-releases/releases/download/1.0.0/litmussdk-1.0.0-py3-none-any.whl" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" },
+ { name = "nats-py", specifier = ">=2.10.0" },
+ { name = "numpy", specifier = ">=2.2.5" },
+ { name = "python-multipart", specifier = ">=0.0.20" },
+]
+
+[package.metadata.requires-dev]
+lint = [
+ { name = "black", specifier = ">=25.1.0" },
+ { name = "radon", specifier = ">=6.0.1" },
+ { name = "ruff", specifier = ">=0.11.4" },
+]
+llm-sdks = [
+ { name = "anthropic", specifier = ">=0.49.0" },
+ { name = "openai-agents", specifier = ">=0.0.13" },
+]
+test = []
+
+[[package]]
+name = "litmussdk"
+version = "1.0.0"
+source = { url = "https://github.com/litmusautomation/litmus-sdk-releases/releases/download/1.0.0/litmussdk-1.0.0-py3-none-any.whl" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+wheels = [
+ { url = "https://github.com/litmusautomation/litmus-sdk-releases/releases/download/1.0.0/litmussdk-1.0.0-py3-none-any.whl", hash = "sha256:1ab04eed5e0d0e13faf46f777ab1c67fff043f9f12647843b8f82bceb45937a3" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+
+[[package]]
+name = "mando"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+]
+
+[[package]]
+name = "mcp"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
+]
+
+[[package]]
+name = "nats-py"
+version = "2.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/2f/0f1b94844760a894659388059bc618b59fd794a3ef2f5113d710864d3fa7/nats_py-2.10.0.tar.gz", hash = "sha256:9d44265a097edb30d40e214c1dd1a7405c1451d33480ce714c041fb73bb66a10", size = 113637 }
+
+[[package]]
+name = "numpy"
+version = "2.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/f7/1fd4ff108cd9d7ef929b8882692e23665dc9c23feecafbb9c6b80f4ec583/numpy-2.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ee461a4eaab4f165b68780a6a1af95fb23a29932be7569b9fab666c407969051", size = 20948633 },
+ { url = "https://files.pythonhosted.org/packages/12/03/d443c278348371b20d830af155ff2079acad6a9e60279fac2b41dbbb73d8/numpy-2.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec31367fd6a255dc8de4772bd1658c3e926d8e860a0b6e922b615e532d320ddc", size = 14176123 },
+ { url = "https://files.pythonhosted.org/packages/2b/0b/5ca264641d0e7b14393313304da48b225d15d471250376f3fbdb1a2be603/numpy-2.2.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:47834cde750d3c9f4e52c6ca28a7361859fcaf52695c7dc3cc1a720b8922683e", size = 5163817 },
+ { url = "https://files.pythonhosted.org/packages/04/b3/d522672b9e3d28e26e1613de7675b441bbd1eaca75db95680635dd158c67/numpy-2.2.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2c1a1c6ccce4022383583a6ded7bbcda22fc635eb4eb1e0a053336425ed36dfa", size = 6698066 },
+ { url = "https://files.pythonhosted.org/packages/a0/93/0f7a75c1ff02d4b76df35079676b3b2719fcdfb39abdf44c8b33f43ef37d/numpy-2.2.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d75f338f5f79ee23548b03d801d28a505198297534f62416391857ea0479571", size = 14087277 },
+ { url = "https://files.pythonhosted.org/packages/b0/d9/7c338b923c53d431bc837b5b787052fef9ae68a56fe91e325aac0d48226e/numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a801fef99668f309b88640e28d261991bfad9617c27beda4a3aec4f217ea073", size = 16135742 },
+ { url = "https://files.pythonhosted.org/packages/2d/10/4dec9184a5d74ba9867c6f7d1e9f2e0fb5fe96ff2bf50bb6f342d64f2003/numpy-2.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:abe38cd8381245a7f49967a6010e77dbf3680bd3627c0fe4362dd693b404c7f8", size = 15581825 },
+ { url = "https://files.pythonhosted.org/packages/80/1f/2b6fcd636e848053f5b57712a7d1880b1565eec35a637fdfd0a30d5e738d/numpy-2.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a0ac90e46fdb5649ab6369d1ab6104bfe5854ab19b645bf5cda0127a13034ae", size = 17899600 },
+ { url = "https://files.pythonhosted.org/packages/ec/87/36801f4dc2623d76a0a3835975524a84bd2b18fe0f8835d45c8eae2f9ff2/numpy-2.2.5-cp312-cp312-win32.whl", hash = "sha256:0cd48122a6b7eab8f06404805b1bd5856200e3ed6f8a1b9a194f9d9054631beb", size = 6312626 },
+ { url = "https://files.pythonhosted.org/packages/8b/09/4ffb4d6cfe7ca6707336187951992bd8a8b9142cf345d87ab858d2d7636a/numpy-2.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:ced69262a8278547e63409b2653b372bf4baff0870c57efa76c5703fd6543282", size = 12645715 },
+ { url = "https://files.pythonhosted.org/packages/e2/a0/0aa7f0f4509a2e07bd7a509042967c2fab635690d4f48c6c7b3afd4f448c/numpy-2.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4", size = 20935102 },
+ { url = "https://files.pythonhosted.org/packages/7e/e4/a6a9f4537542912ec513185396fce52cdd45bdcf3e9d921ab02a93ca5aa9/numpy-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f", size = 14191709 },
+ { url = "https://files.pythonhosted.org/packages/be/65/72f3186b6050bbfe9c43cb81f9df59ae63603491d36179cf7a7c8d216758/numpy-2.2.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9", size = 5149173 },
+ { url = "https://files.pythonhosted.org/packages/e5/e9/83e7a9432378dde5802651307ae5e9ea07bb72b416728202218cd4da2801/numpy-2.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191", size = 6684502 },
+ { url = "https://files.pythonhosted.org/packages/ea/27/b80da6c762394c8ee516b74c1f686fcd16c8f23b14de57ba0cad7349d1d2/numpy-2.2.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372", size = 14084417 },
+ { url = "https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d", size = 16133807 },
+ { url = "https://files.pythonhosted.org/packages/bf/9b/4cc171a0acbe4666f7775cfd21d4eb6bb1d36d3a0431f48a73e9212d2278/numpy-2.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7", size = 15575611 },
+ { url = "https://files.pythonhosted.org/packages/a3/45/40f4135341850df48f8edcf949cf47b523c404b712774f8855a64c96ef29/numpy-2.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73", size = 17895747 },
+ { url = "https://files.pythonhosted.org/packages/f8/4c/b32a17a46f0ffbde8cc82df6d3daeaf4f552e346df143e1b188a701a8f09/numpy-2.2.5-cp313-cp313-win32.whl", hash = "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b", size = 6309594 },
+ { url = "https://files.pythonhosted.org/packages/13/ae/72e6276feb9ef06787365b05915bfdb057d01fceb4a43cb80978e518d79b/numpy-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471", size = 12638356 },
+ { url = "https://files.pythonhosted.org/packages/79/56/be8b85a9f2adb688e7ded6324e20149a03541d2b3297c3ffc1a73f46dedb/numpy-2.2.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6", size = 20963778 },
+ { url = "https://files.pythonhosted.org/packages/ff/77/19c5e62d55bff507a18c3cdff82e94fe174957bad25860a991cac719d3ab/numpy-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba", size = 14207279 },
+ { url = "https://files.pythonhosted.org/packages/75/22/aa11f22dc11ff4ffe4e849d9b63bbe8d4ac6d5fae85ddaa67dfe43be3e76/numpy-2.2.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133", size = 5199247 },
+ { url = "https://files.pythonhosted.org/packages/4f/6c/12d5e760fc62c08eded0394f62039f5a9857f758312bf01632a81d841459/numpy-2.2.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376", size = 6711087 },
+ { url = "https://files.pythonhosted.org/packages/ef/94/ece8280cf4218b2bee5cec9567629e61e51b4be501e5c6840ceb593db945/numpy-2.2.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19", size = 14059964 },
+ { url = "https://files.pythonhosted.org/packages/39/41/c5377dac0514aaeec69115830a39d905b1882819c8e65d97fc60e177e19e/numpy-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0", size = 16121214 },
+ { url = "https://files.pythonhosted.org/packages/db/54/3b9f89a943257bc8e187145c6bc0eb8e3d615655f7b14e9b490b053e8149/numpy-2.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a", size = 15575788 },
+ { url = "https://files.pythonhosted.org/packages/b1/c4/2e407e85df35b29f79945751b8f8e671057a13a376497d7fb2151ba0d290/numpy-2.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066", size = 17893672 },
+ { url = "https://files.pythonhosted.org/packages/29/7e/d0b44e129d038dba453f00d0e29ebd6eaf2f06055d72b95b9947998aca14/numpy-2.2.5-cp313-cp313t-win32.whl", hash = "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e", size = 6377102 },
+ { url = "https://files.pythonhosted.org/packages/63/be/b85e4aa4bf42c6502851b971f1c326d583fcc68227385f92089cf50a7b45/numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", size = 12750096 },
+]
+
+[[package]]
+name = "openai"
+version = "1.76.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/48/e767710b07acc1fca1f6b8cacd743102c71b8fdeca603876de0749ec00f1/openai-1.76.2.tar.gz", hash = "sha256:f430c8b848775907405c6eff54621254c96f6444c593c097e0cc3a9f8fdda96f", size = 434922 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/5f/aecb820917e93ca9fcac408e998dc22ee0561c308ed58dc8f328e3f7ef14/openai-1.76.2-py3-none-any.whl", hash = "sha256:9c1d9ad59e6e3bea7205eedc9ca66eeebae18d47b527e505a2b0d2fb1538e26e", size = 661253 },
+]
+
+[[package]]
+name = "openai-agents"
+version = "0.0.13"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe" },
+ { name = "mcp" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "types-requests" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/6e/3e14abef846b9aaaa454d0c2e3353e0c5b4c72806633bf193024319806f3/openai_agents-0.0.13.tar.gz", hash = "sha256:6b80315e75c06b5302c5f2adba2f9ea3845f94615daed4706bfb871740f561a5", size = 1338862 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/c7/501f5bba74384f9bd3c6fee6ae2d1e48e83402dbe058c34aaf7d32e0af15/openai_agents-0.0.13-py3-none-any.whl", hash = "sha256:e11910679e74803e8a4237ce52a21ee6f9ef0848d866e8198f5c4fb8c6310204", size = 116788 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.3"
+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/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
+ { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
+ { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
+ { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
+ { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
+ { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
+ { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
+ { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
+ { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
+ { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
+ { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
+ { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
+ { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
+ { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
+ { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
+ { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
+ { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
+ { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
+ { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
+ { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
+ { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
+ { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
+ { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
+ { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
+ { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
+ { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
+ { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
+ { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
+ { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
+ { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
+ { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.8.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "radon"
+version = "6.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+ { name = "mando" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784 },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
+[[package]]
+name = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.24.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945 },
+ { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935 },
+ { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817 },
+ { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983 },
+ { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719 },
+ { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546 },
+ { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695 },
+ { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218 },
+ { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062 },
+ { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262 },
+ { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 },
+ { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 },
+ { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 },
+ { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 },
+ { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 },
+ { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 },
+ { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 },
+ { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 },
+ { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 },
+ { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 },
+ { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 },
+ { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 },
+ { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 },
+ { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 },
+ { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 },
+ { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 },
+ { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 },
+ { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 },
+ { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 },
+ { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 },
+ { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 },
+ { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 },
+ { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 },
+ { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 },
+ { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 },
+ { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 },
+ { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 },
+ { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 },
+ { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 },
+]
+
+[[package]]
+name = "ruff"
+version = "0.11.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 },
+ { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 },
+ { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 },
+ { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 },
+ { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 },
+ { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 },
+ { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 },
+ { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 },
+ { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 },
+ { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 },
+ { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 },
+ { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 },
+ { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 },
+ { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 },
+ { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 },
+ { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 },
+ { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
+]
+
+[[package]]
+name = "typer"
+version = "0.15.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
+]
+
+[[package]]
+name = "types-requests"
+version = "2.32.0.20250328"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.34.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
+]