Sandra Sanchez commited on
Commit
c7d933a
·
1 Parent(s): af826f9

Integrate mcp server into Claude desktop SDK by editing config and using easy formula with fastmcp

Browse files
Files changed (9) hide show
  1. app.py +20 -72
  2. claude_mcp_tools.json +0 -53
  3. launcher.py +0 -33
  4. mcp_api.py +0 -61
  5. mcp_server/__init__.py +0 -0
  6. mcp_server/logic.py +0 -108
  7. pyproject.toml +1 -0
  8. server.py +284 -0
  9. uv.lock +352 -4
app.py CHANGED
@@ -7,9 +7,8 @@ from dotenv import load_dotenv
7
  import base64
8
  import io
9
  from PIL import Image
10
- import asyncio
11
- from mcp_server.logic import create_mcp_server
12
  import tempfile
 
13
 
14
  # Load environment variables from .env file
15
  load_dotenv()
@@ -20,15 +19,9 @@ client = OpenAI(api_key=OPENAI_API_KEY)
20
  TEMPLATES_DIR = Path(__file__).resolve().parent / "mcp_server" / "templates"
21
  TEMP_DIR = Path(__file__).resolve().parent / "temp"
22
  TEMP_DIR.mkdir(exist_ok=True)
 
23
 
24
- VIBE_DESCRIPTIONS = {
25
- "Comic": "comic-style illustration, comic strip format with exactly 4 panels, bold outlines, dynamic poses, speech bubbles, vibrant colors. If there is text, translate it to the selected language with perfect ortography",
26
- "Kawaii": "kawaii-style illustration, pastel colors, cute rounded characters, big eyes.",
27
- "Pictorial": "pictorial illustration, painted with a brush, gentle and calm atmosphere",
28
- "Basic B&W": "black and white illustration, simple lines, no other colors allowed, like a coloring book.Absolutely only use black and white.",
29
- "Cartoon": "cartoon-style illustration, exaggerated features, bright colors, playful mood",
30
- "Soft Pastel": "Only use soft pastel colours, smooth textures, calming feeling"
31
- }
32
 
33
  def load_scenarios():
34
  # Returns the list of available scenarios
@@ -58,75 +51,28 @@ def generate_story_base(scenario_name: str, language="en", culture="default", ag
58
  model="gpt-4o-mini",
59
  messages=[{"role": "user", "content": story_prompt}]
60
  )
61
- print(f"[DEBUG] Story response:\n{story_resp.choices[0].message.content.strip()}")
62
- return story_resp.choices[0].message.content.strip()
 
63
 
64
- def generate_image(scenario_name: str, culture="default", age="7", gender="female", vibe="Cartoon", comfort_character="Koala", story_text=None, language="en"):
65
- # Generates the illustration based on the story
66
- scenario_file = scenario_name.replace(" ", "_")
67
- filepath = TEMPLATES_DIR / f"{scenario_file}.json"
68
- if not filepath.exists():
69
- return None
70
- template = json.loads(filepath.read_text())
71
- print(f"[DEBUG] Template loaded for image: {template}")
72
- vibe_desc = VIBE_DESCRIPTIONS.get(vibe, vibe)
73
- # Traducir solo la historia si es necesario
74
- story_for_image = story_text
75
- lang = language if language is not None else "en"
76
- if lang.lower() != "en":
77
- try:
78
- loop = asyncio.get_event_loop()
79
- except RuntimeError:
80
- loop = asyncio.new_event_loop()
81
- asyncio.set_event_loop(loop)
82
- story_for_image = loop.run_until_complete(call_translate_tool(story_text, lang))
83
- print(f"[DEBUG] Translated story for image:\n{story_for_image}")
84
- if vibe == "Comic":
85
- image_prompt = (
86
- f"STORY (in {lang}): {story_for_image}\n"
87
- f"INSTRUCTIONS (in English): FORMAT: Create a comic strip with exactly 4 panels. Each panel should have at most one speech bubble, and no more than 3 speech bubbles in total. Each speech bubble should contain only 1 to 3 words, in perfect {lang} orthography. The comic must visually and textually reflect the story above. Show a child of age {age}, gender {gender}, and the comfort character {comfort_character} together in a safe, public, and friendly environment. Do not include any other animals. Use the story as inspiration for the actions and words in the comic. Context: {template['title']}. Culture: {culture}. Illustration style: {vibe_desc}."
88
- )
89
- else:
90
- image_prompt = (
91
- f"STORY (in {lang}): {story_for_image}\n"
92
- f"INSTRUCTIONS (in English): FORMAT: This is NOT a comic. Do NOT use panels, speech bubbles, or any text. Only draw a single illustration. Create an illustration in the style '{vibe}': {vibe_desc}. Show a child of age {age}, gender {gender}, and the comfort character {comfort_character} together in a safe, public and friendly environment. Do not include any other animals. Context: {template['title']}. Culture: {culture}. Only visual elements, no text. Key features: 5 visual features of the style. TONE AND MOOD: Gentle, supportive, calming. Friendly characters."
93
- )
94
- print(f"[DEBUG] Image prompt:\n{image_prompt}\nLanguage: {lang}")
95
- img = client.images.generate(
96
- model="gpt-image-1",
97
- prompt=image_prompt,
98
- output_format="png",
99
- size="1024x1024"
100
- )
101
- # Decode image from base64
102
- image_bytes = base64.b64decode(img.data[0].b64_json)
103
- image = Image.open(io.BytesIO(image_bytes))
104
- return image
105
 
106
  def show_selected(scenario_name):
107
  # Shows the selected scenario
108
  return f"You selected: {scenario_name}"
109
 
110
- async def call_adapt_tool(story, culture, age, gender, vibe, comfort_character):
 
111
  print(f"\n[TOOL] Adapt story called with:\nCulture: {culture}, Age: {age}, Gender: {gender}, Vibe: {vibe}, Comfort Character: {comfort_character}\nStory:\n{story}\n")
112
- server = await create_mcp_server()
113
- context = {}
114
- result = await server.request_handlers["adapt_story"](context, story, culture, age, gender, vibe, comfort_character)
115
- return result["adapted_story"]
116
 
117
- async def call_translate_tool(story, language, gender="female"):
118
  print(f"\n[TOOL] Translate story called with:\nLanguage: {language}\nGender: {gender}\nStory:\n{story}\n")
119
- server = await create_mcp_server()
120
- context = {}
121
- result = await server.request_handlers["translate_story"](context, story, language, gender)
122
- return result["translated_story"]
123
 
124
- async def call_voice_tool(story, language):
125
  print(f"\n[TOOL] Generate voice called with:\nLanguage: {language}\nStory:\n{story}\n")
126
- server = await create_mcp_server()
127
- context = {}
128
- result = await server.request_handlers["generate_voice"](context, story, language)
129
- return result.get("audio", None)
130
 
131
  def format_story(story):
132
  # Formats the text for large, clear display, without asterisks
@@ -173,9 +119,9 @@ def main():
173
  story_base = generate_story_base(
174
  scenario_name, language, culture, age, gender, vibe, comfort_character
175
  )
176
- adapted_story = asyncio.run(call_adapt_tool(story_base, culture, age, gender, vibe, comfort_character))
177
  if language != "en":
178
- final_story = asyncio.run(call_translate_tool(adapted_story, language, gender))
179
  else:
180
  final_story = adapted_story
181
  return format_story(final_story), final_story
@@ -191,7 +137,9 @@ def main():
191
  def on_generate_image(scenario_name, culture, age, gender, vibe, comfort_character, story_text, language):
192
  print(f"[DEBUG] Received args in on_generate_image: scenario_name={scenario_name}, culture={culture}, age={age}, gender={gender}, vibe={vibe}, comfort_character={comfort_character}, language={language}")
193
  print(f"[DEBUG] Story text for image:\n{story_text}")
194
- return generate_image(scenario_name, culture, age, gender, vibe, comfort_character, story_text=story_text, language=language)
 
 
195
 
196
  image_btn.click(
197
  fn=on_generate_image,
@@ -207,7 +155,7 @@ def main():
207
  def on_voice(story, language):
208
  if not story or story.strip() == "":
209
  return None
210
- audio_bytes = asyncio.run(call_voice_tool(story, language))
211
  if audio_bytes:
212
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3", dir=str(TEMP_DIR)) as tmp_file:
213
  tmp_file.write(audio_bytes)
 
7
  import base64
8
  import io
9
  from PIL import Image
 
 
10
  import tempfile
11
+ from server import adapt_story, translate_story, generate_voice, generate_image as generate_image_mcp
12
 
13
  # Load environment variables from .env file
14
  load_dotenv()
 
19
  TEMPLATES_DIR = Path(__file__).resolve().parent / "mcp_server" / "templates"
20
  TEMP_DIR = Path(__file__).resolve().parent / "temp"
21
  TEMP_DIR.mkdir(exist_ok=True)
22
+ GENERATED_IMAGES_DIR = Path(__file__).resolve().parent / "generated_images"
23
 
24
+ # VIBE_DESCRIPTIONS moved to server.py
 
 
 
 
 
 
 
25
 
26
  def load_scenarios():
27
  # Returns the list of available scenarios
 
51
  model="gpt-4o-mini",
52
  messages=[{"role": "user", "content": story_prompt}]
53
  )
54
+ story_base = story_resp.choices[0].message.content.strip()
55
+ print(f"[DEBUG] Story response:\n{story_base}")
56
+ return story_base
57
 
58
+ # generate_image now imported from server.py as generate_image_mcp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  def show_selected(scenario_name):
61
  # Shows the selected scenario
62
  return f"You selected: {scenario_name}"
63
 
64
+ # Direct tool calls (no async needed)
65
+ def call_adapt_tool(story, culture, age, gender, vibe, comfort_character):
66
  print(f"\n[TOOL] Adapt story called with:\nCulture: {culture}, Age: {age}, Gender: {gender}, Vibe: {vibe}, Comfort Character: {comfort_character}\nStory:\n{story}\n")
67
+ return adapt_story(story, culture, age, gender, vibe, comfort_character)
 
 
 
68
 
69
+ def call_translate_tool(story, language, gender="female"):
70
  print(f"\n[TOOL] Translate story called with:\nLanguage: {language}\nGender: {gender}\nStory:\n{story}\n")
71
+ return translate_story(story, language, gender)
 
 
 
72
 
73
+ def call_voice_tool(story, language):
74
  print(f"\n[TOOL] Generate voice called with:\nLanguage: {language}\nStory:\n{story}\n")
75
+ return generate_voice(story, language)
 
 
 
76
 
77
  def format_story(story):
78
  # Formats the text for large, clear display, without asterisks
 
119
  story_base = generate_story_base(
120
  scenario_name, language, culture, age, gender, vibe, comfort_character
121
  )
122
+ adapted_story = call_adapt_tool(story_base, culture, age, gender, vibe, comfort_character)
123
  if language != "en":
124
+ final_story = call_translate_tool(adapted_story, language, gender)
125
  else:
126
  final_story = adapted_story
127
  return format_story(final_story), final_story
 
137
  def on_generate_image(scenario_name, culture, age, gender, vibe, comfort_character, story_text, language):
138
  print(f"[DEBUG] Received args in on_generate_image: scenario_name={scenario_name}, culture={culture}, age={age}, gender={gender}, vibe={vibe}, comfort_character={comfort_character}, language={language}")
139
  print(f"[DEBUG] Story text for image:\n{story_text}")
140
+ image_path = generate_image_mcp(scenario_name, culture, age, gender, vibe, comfort_character, story_text, language)
141
+ # Load and return the image
142
+ return Image.open(image_path)
143
 
144
  image_btn.click(
145
  fn=on_generate_image,
 
155
  def on_voice(story, language):
156
  if not story or story.strip() == "":
157
  return None
158
+ audio_bytes = call_voice_tool(story, language)
159
  if audio_bytes:
160
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3", dir=str(TEMP_DIR)) as tmp_file:
161
  tmp_file.write(audio_bytes)
claude_mcp_tools.json DELETED
@@ -1,53 +0,0 @@
1
- [
2
- {
3
- "name": "adapt_story",
4
- "description": "Adapts a story for an autistic child based on culture, age, gender, and comfort character.",
5
- "endpoint": "http://127.0.0.1:8000/adapt_story",
6
- "method": "POST",
7
- "args": {
8
- "story": "string",
9
- "culture": "string",
10
- "age": "string",
11
- "gender": "string",
12
- "vibe": "string",
13
- "comfort_character": "string"
14
- }
15
- },
16
- {
17
- "name": "translate_story",
18
- "description": "Translates a story to the selected language and gender.",
19
- "endpoint": "http://127.0.0.1:8000/translate_story",
20
- "method": "POST",
21
- "args": {
22
- "story": "string",
23
- "language": "string",
24
- "gender": "string"
25
- }
26
- },
27
- {
28
- "name": "generate_voice",
29
- "description": "Generates audio for the story in the selected language.",
30
- "endpoint": "http://127.0.0.1:8000/generate_voice",
31
- "method": "POST",
32
- "args": {
33
- "story": "string",
34
- "language": "string"
35
- }
36
- },
37
- {
38
- "name": "generate_image",
39
- "description": "Generates an illustration based on the story and parameters.",
40
- "endpoint": "http://127.0.0.1:8000/generate_image",
41
- "method": "POST",
42
- "args": {
43
- "scenario_name": "string",
44
- "culture": "string",
45
- "age": "string",
46
- "gender": "string",
47
- "vibe": "string",
48
- "comfort_character": "string",
49
- "story_text": "string",
50
- "language": "string"
51
- }
52
- }
53
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
launcher.py DELETED
@@ -1,33 +0,0 @@
1
- # launcher.py
2
- import asyncio
3
- from mcp.server.stdio import stdio_server
4
- from mcp_server.logic import create_mcp_server
5
- import logging
6
- import sys
7
- import os
8
-
9
- logging.basicConfig(level=logging.INFO)
10
- logger = logging.getLogger(__name__)
11
-
12
- # Add project root to path
13
- ROOT = os.path.dirname(os.path.abspath(__file__))
14
- sys.path.append(ROOT)
15
-
16
-
17
- async def main():
18
- logger.info("Starting MCP server...")
19
- server = await create_mcp_server()
20
- async with stdio_server() as (read_stream, write_stream):
21
- logger.info("Server connected, running...")
22
- await server.run(
23
- read_stream,
24
- write_stream,
25
- server.create_initialization_options()
26
- )
27
-
28
- print("[MCP] MCP server started.")
29
-
30
- if __name__ == "__main__":
31
- asyncio.run(main())
32
-
33
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mcp_api.py DELETED
@@ -1,61 +0,0 @@
1
- from fastapi import FastAPI, Request
2
- from mcp_server.logic import create_mcp_server
3
- import asyncio
4
- import sys
5
-
6
-
7
- print("MCP server starting...", file=sys.stderr)
8
-
9
- app = FastAPI()
10
-
11
- @app.post("/adapt_story")
12
- async def adapt_story_endpoint(request: Request):
13
- data = await request.json()
14
- story = data.get("story")
15
- culture = data.get("culture", "default")
16
- age = data.get("age", "7")
17
- gender = data.get("gender", "female")
18
- vibe = data.get("vibe", "Cartoon")
19
- comfort_character = data.get("comfort_character", "Koala")
20
- server = await create_mcp_server()
21
- context = {}
22
- result = await server.request_handlers["adapt_story"](context, story, culture, age, gender, vibe, comfort_character)
23
- return result
24
-
25
- @app.post("/translate_story")
26
- async def translate_story_endpoint(request: Request):
27
- data = await request.json()
28
- story = data.get("story")
29
- language = data.get("language", "en")
30
- gender = data.get("gender", "female")
31
- server = await create_mcp_server()
32
- context = {}
33
- result = await server.request_handlers["translate_story"](context, story, language, gender)
34
- return result
35
-
36
- @app.post("/generate_voice")
37
- async def generate_voice_endpoint(request: Request):
38
- data = await request.json()
39
- story = data.get("story")
40
- language = data.get("language", "en")
41
- server = await create_mcp_server()
42
- context = {}
43
- result = await server.request_handlers["generate_voice"](context, story, language)
44
- return result
45
-
46
- @app.post("/generate_image")
47
- async def generate_image_endpoint(request: Request):
48
- data = await request.json()
49
- scenario_name = data.get("scenario_name")
50
- culture = data.get("culture", "default")
51
- age = data.get("age", "7")
52
- gender = data.get("gender", "female")
53
- vibe = data.get("vibe", "Cartoon")
54
- comfort_character = data.get("comfort_character", "Koala")
55
- story_text = data.get("story_text")
56
- language = data.get("language", "en")
57
- from app import generate_image
58
- image = generate_image(scenario_name, culture, age, gender, vibe, comfort_character, story_text, language)
59
- return {"image": "[image generated]"}
60
-
61
- # Para ejecutar: uvicorn mcp_api:app --reload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mcp_server/__init__.py DELETED
File without changes
mcp_server/logic.py DELETED
@@ -1,108 +0,0 @@
1
- # mcp_server/server.py
2
- from mcp.server import Server
3
- from mcp.server.stdio import stdio_server
4
- import os
5
- from openai import OpenAI
6
-
7
- async def create_mcp_server():
8
- server = Server(
9
- name="comfortool-mcp"
10
- )
11
-
12
- # If your SDK has setters, use them:
13
- if hasattr(server, "set_description"):
14
- server.set_description("MCP server for comfortool")
15
-
16
- if hasattr(server, "set_version"):
17
- server.set_version("0.1.0")
18
-
19
- # Tool: Adapt a story for autism and culture
20
- async def adapt_story(context, story, culture="default", age="7", gender="female", vibe="Cartoon", comfort_character="Koala"):
21
- prompt = (
22
- f"Adapt the following story for an autistic child of age {age}, gender {gender}, from {culture} culture."
23
- f"Include '{comfort_character}' as a supportive friend, use the style '{vibe}', and make the story concrete, supportive, and easy to understand."
24
- "Avoid excessive emotion and exclamation marks."
25
- "Adapt language according to age, simpler sentences for smaller (ages 2 to 4)kids"
26
- "The absolute max length is 640 characters"
27
- "Do not mention any info related to format, like pages, panels or scenes. It is just one story"
28
- "Do not translate. Return only the adapted story in English."
29
- f"\n\nStory:\n{story}"
30
- )
31
- openai_api_key = os.environ.get("OPENAI_API_KEY")
32
- client = OpenAI(api_key=openai_api_key)
33
- response = client.chat.completions.create(
34
- model="gpt-4o-mini",
35
- messages=[{"role": "user", "content": prompt}]
36
- )
37
- adapted_story = response.choices[0].message.content.strip()
38
- return {"adapted_story": adapted_story}
39
-
40
- # Tool: Translate a story
41
- async def translate_story(context, story, language="es-ES", gender="female"):
42
- prompt = (
43
- f"Translate the following story to {language} and the corresponding regional or cultural variant if mentioned. Use correct grammatical gender and pronouns, adapted to the selected gender of the kid ('{gender}'). "
44
- "If the story mentions an animal or character, translate its name to the most culturally and linguistically appropriate version for the target language and region. "
45
- "For example, if the story mentions 'fox', translate it as 'zorro' for Spanish from Spain. "
46
- "Return only the translated story, using the regional variety requested. "
47
- f"\n\nStory:\n{story}"
48
- )
49
- openai_api_key = os.environ.get("OPENAI_API_KEY")
50
- client = OpenAI(api_key=openai_api_key)
51
- response = client.chat.completions.create(
52
- model="gpt-4o-mini",
53
- messages=[{"role": "user", "content": prompt}]
54
- )
55
- translated_story = response.choices[0].message.content.strip()
56
- return {"translated_story": translated_story}
57
-
58
- # Tool: Adaptive TTS with LLM-based smart selection
59
- VALID_VOICES = {"nova", "shimmer", "echo", "onyx", "fable", "alloy", "ash", "sage", "coral"}
60
-
61
- async def generate_voice(context, story, language="en"):
62
- openai_api_key = os.environ.get("OPENAI_API_KEY")
63
- client = OpenAI(api_key=openai_api_key)
64
- prompt = (
65
- f"Read the following story aloud for a child in language '{language}'. "
66
- "Select ONLY one of these OpenAI voices: nova, shimmer, echo, onyx, fable, alloy, ash, sage, coral. "
67
- "Adapt pronunciation and prosody to match the regional variety (for example, Spanish from Spain vs. Mexico, English US vs. UK) with its specific phonemes, lie Spanish Z. "
68
- "Decide what is the right pronunciation choice in case there are foreign words, like talking about a corgi dog in Spanish"
69
- "If no specific voice is available, use the default for the language. "
70
- "Make the speech clear, friendly, and easy to understand for autistic children. "
71
- "Return only the name of the selected voice in the format: Voice: <voice_name>."
72
- f"\n\nStory:\n{story}"
73
- )
74
- response = client.chat.completions.create(
75
- model="gpt-4o-mini",
76
- messages=[{"role": "user", "content": prompt}]
77
- )
78
- voice_decision = response.choices[0].message.content.strip()
79
- print(f"[VOICE LLM DECISION]: {voice_decision}")
80
-
81
- voice = "nova" # Default value
82
- for line in voice_decision.splitlines():
83
- if line.lower().startswith("voice:"):
84
- candidate = line.split(":", 1)[1].strip().strip('"').strip("'")
85
- if candidate in VALID_VOICES:
86
- voice = candidate
87
- else:
88
- print(f"[VOICE WARNING] '{candidate}' is not a valid OpenAI voice. Using default: 'nova'.")
89
- break
90
-
91
- tts_response = client.audio.speech.create(
92
- model="tts-1",
93
- voice=voice,
94
- input=story,
95
- response_format="mp3"
96
- )
97
- audio_bytes = tts_response.content
98
- return {"audio": audio_bytes}
99
-
100
-
101
- server.request_handlers["adapt_story"] = adapt_story
102
- server.request_handlers["translate_story"] = translate_story
103
- server.request_handlers["generate_voice"] = generate_voice
104
-
105
- return server
106
-
107
-
108
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -14,6 +14,7 @@ dependencies = [
14
  "uvicorn>=0.38.0",
15
  "mcp>=1.22.0",
16
  "openai>=2.8.1",
 
17
  ]
18
 
19
  [tool.uv]
 
14
  "uvicorn>=0.38.0",
15
  "mcp>=1.22.0",
16
  "openai>=2.8.1",
17
+ "fastmcp>=2.13.2",
18
  ]
19
 
20
  [tool.uv]
server.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Comfortool MCP Server
3
+ Provides tools for creating autism-friendly social stories with cultural adaptation
4
+ """
5
+ from fastmcp import FastMCP
6
+ from openai import OpenAI
7
+ import os
8
+ from dotenv import load_dotenv
9
+ import base64
10
+ import io
11
+ from PIL import Image
12
+ from pathlib import Path
13
+ import json
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+
18
+ # Initialize FastMCP server
19
+ mcp = FastMCP("comfortool")
20
+
21
+ # Constants
22
+ TEMPLATES_DIR = Path(__file__).resolve().parent / "mcp_server" / "templates"
23
+ GENERATED_IMAGES_DIR = Path(__file__).resolve().parent / "generated_images"
24
+ GENERATED_IMAGES_DIR.mkdir(exist_ok=True)
25
+
26
+ VIBE_DESCRIPTIONS = {
27
+ "Comic": "comic-style illustration, comic strip format with exactly 4 panels, bold outlines, dynamic poses, speech bubbles, vibrant colors. If there is text, translate it to the selected language with perfect ortography",
28
+ "Kawaii": "kawaii-style illustration, pastel colors, cute rounded characters, big eyes.",
29
+ "Pictorial": "pictorial illustration, painted with a brush, gentle and calm atmosphere",
30
+ "Basic B&W": "black and white illustration, simple lines, no other colors allowed, like a coloring book.Absolutely only use black and white.",
31
+ "Cartoon": "cartoon-style illustration, exaggerated features, bright colors, playful mood",
32
+ "Soft Pastel": "Only use soft pastel colours, smooth textures, calming feeling"
33
+ }
34
+
35
+ VALID_VOICES = {"nova", "shimmer", "echo", "onyx", "fable", "alloy", "ash", "sage", "coral"}
36
+
37
+ # Initialize OpenAI client
38
+ def get_openai_client():
39
+ api_key = os.environ.get("OPENAI_API_KEY")
40
+ if not api_key:
41
+ raise ValueError("OPENAI_API_KEY not found in environment variables")
42
+ return OpenAI(api_key=api_key)
43
+
44
+
45
+ @mcp.tool()
46
+ def adapt_story(
47
+ story: str,
48
+ culture: str = "default",
49
+ age: str = "7",
50
+ gender: str = "female",
51
+ vibe: str = "Cartoon",
52
+ comfort_character: str = "Koala"
53
+ ) -> str:
54
+ """
55
+ Adapts a story for an autistic child based on culture, age, gender, and comfort character.
56
+
57
+ Args:
58
+ story: The original story text to adapt
59
+ culture: Cultural context (e.g., 'Latino', 'Roma', 'Muslim', 'default')
60
+ age: Child's age (e.g., '7')
61
+ gender: Gender identity (e.g., 'boy', 'girl', 'non-binary')
62
+ vibe: Illustration style for the story
63
+ comfort_character: Supportive character/animal (e.g., 'Koala', 'Robot')
64
+
65
+ Returns:
66
+ The adapted story in English
67
+ """
68
+ prompt = (
69
+ f"Adapt the following story for an autistic child of age {age}, gender {gender}, from {culture} culture. "
70
+ f"Include '{comfort_character}' as a supportive friend, use the style '{vibe}', and make the story concrete, supportive, and easy to understand. "
71
+ "Avoid excessive emotion and exclamation marks. "
72
+ "Adapt language according to age, simpler sentences for smaller (ages 2 to 4) kids. "
73
+ "The absolute max length is 640 characters. "
74
+ "Do not mention any info related to format, like pages, panels or scenes. It is just one story. "
75
+ "Do not translate. Return only the adapted story in English."
76
+ f"\n\nStory:\n{story}"
77
+ )
78
+
79
+ client = get_openai_client()
80
+ response = client.chat.completions.create(
81
+ model="gpt-4o-mini",
82
+ messages=[{"role": "user", "content": prompt}]
83
+ )
84
+
85
+ adapted_story = response.choices[0].message.content.strip()
86
+ return adapted_story
87
+
88
+
89
+ @mcp.tool()
90
+ def translate_story(
91
+ story: str,
92
+ language: str = "en",
93
+ gender: str = "female"
94
+ ) -> str:
95
+ """
96
+ Translates a story to the selected language with proper grammatical gender.
97
+
98
+ Args:
99
+ story: The story text to translate
100
+ language: Target language (e.g., 'Spanish from Spain', 'en', 'fr')
101
+ gender: Gender for grammatical agreement (e.g., 'boy', 'girl', 'non-binary')
102
+
103
+ Returns:
104
+ The translated story
105
+ """
106
+ if language.lower() == "en":
107
+ return story
108
+
109
+ prompt = (
110
+ f"Translate the following story to {language} and the corresponding regional or cultural variant if mentioned. "
111
+ f"Use correct grammatical gender and pronouns, adapted to the selected gender of the kid ('{gender}'). "
112
+ "If the story mentions an animal or character, translate its name to the most culturally and linguistically appropriate version for the target language and region. "
113
+ "For example, if the story mentions 'fox', translate it as 'zorro' for Spanish from Spain. "
114
+ "Return only the translated story, using the regional variety requested."
115
+ f"\n\nStory:\n{story}"
116
+ )
117
+
118
+ client = get_openai_client()
119
+ response = client.chat.completions.create(
120
+ model="gpt-4o-mini",
121
+ messages=[{"role": "user", "content": prompt}]
122
+ )
123
+
124
+ translated_story = response.choices[0].message.content.strip()
125
+ return translated_story
126
+
127
+
128
+ @mcp.tool()
129
+ def generate_voice(
130
+ story: str,
131
+ language: str = "en"
132
+ ) -> bytes:
133
+ """
134
+ Generates audio narration of the story using adaptive TTS with LLM-based voice selection.
135
+
136
+ Args:
137
+ story: The story text to narrate
138
+ language: Language for pronunciation (e.g., 'en', 'Spanish from Spain')
139
+
140
+ Returns:
141
+ Audio bytes in MP3 format
142
+ """
143
+ client = get_openai_client()
144
+
145
+ # Use LLM to select the best voice
146
+ prompt = (
147
+ f"Read the following story aloud for a child in language '{language}'. "
148
+ "Select ONLY one of these OpenAI voices: nova, shimmer, echo, onyx, fable, alloy, ash, sage, coral. "
149
+ "Adapt pronunciation and prosody to match the regional variety (for example, Spanish from Spain vs. Mexico, English US vs. UK) with its specific phonemes, like Spanish Z. "
150
+ "Decide what is the right pronunciation choice in case there are foreign words, like talking about a corgi dog in Spanish. "
151
+ "If no specific voice is available, use the default for the language. "
152
+ "Make the speech clear, friendly, and easy to understand for autistic children. "
153
+ "Return only the name of the selected voice in the format: Voice: <voice_name>."
154
+ f"\n\nStory:\n{story}"
155
+ )
156
+
157
+ response = client.chat.completions.create(
158
+ model="gpt-4o-mini",
159
+ messages=[{"role": "user", "content": prompt}]
160
+ )
161
+
162
+ voice_decision = response.choices[0].message.content.strip()
163
+ print(f"[VOICE LLM DECISION]: {voice_decision}")
164
+
165
+ # Parse voice selection
166
+ voice = "nova" # Default
167
+ for line in voice_decision.splitlines():
168
+ if line.lower().startswith("voice:"):
169
+ candidate = line.split(":", 1)[1].strip().strip('"').strip("'")
170
+ if candidate in VALID_VOICES:
171
+ voice = candidate
172
+ else:
173
+ print(f"[VOICE WARNING] '{candidate}' is not a valid OpenAI voice. Using default: 'nova'.")
174
+ break
175
+
176
+ # Generate TTS
177
+ tts_response = client.audio.speech.create(
178
+ model="tts-1",
179
+ voice=voice,
180
+ input=story,
181
+ response_format="mp3"
182
+ )
183
+
184
+ return tts_response.content
185
+
186
+
187
+ @mcp.tool()
188
+ def generate_image(
189
+ scenario_name: str,
190
+ culture: str = "default",
191
+ age: str = "7",
192
+ gender: str = "female",
193
+ vibe: str = "Cartoon",
194
+ comfort_character: str = "Koala",
195
+ story_text: str = "",
196
+ language: str = "en"
197
+ ) -> str:
198
+ """
199
+ Generates an illustration based on a scenario and story parameters.
200
+
201
+ Args:
202
+ scenario_name: Name of the scenario template (e.g., 'first day school')
203
+ culture: Cultural context
204
+ age: Child's age
205
+ gender: Gender identity
206
+ vibe: Illustration style (Comic, Kawaii, Pictorial, Basic B&W, Cartoon, Soft Pastel)
207
+ comfort_character: Supportive character/animal
208
+ story_text: The story text to illustrate
209
+ language: Language for any text in the image
210
+
211
+ Returns:
212
+ Path to the generated image file
213
+ """
214
+ # Load scenario template
215
+ # Handle both "scenario name" and "scenario_name" formats
216
+ scenario_file = scenario_name.replace(" ", "_").replace("-", "_")
217
+ filepath = TEMPLATES_DIR / f"{scenario_file}.json"
218
+
219
+ if not filepath.exists():
220
+ raise FileNotFoundError(f"Template not found: {scenario_file}. Available scenarios: {[p.stem for p in TEMPLATES_DIR.glob('*.json')]}")
221
+
222
+ template = json.loads(filepath.read_text())
223
+ vibe_desc = VIBE_DESCRIPTIONS.get(vibe, vibe)
224
+
225
+ # Translate story if needed
226
+ story_for_image = story_text
227
+ if language.lower() != "en" and story_text:
228
+ story_for_image = translate_story(story_text, language, gender)
229
+
230
+ # Create image prompt based on vibe
231
+ client = get_openai_client()
232
+
233
+ if vibe == "Comic":
234
+ image_prompt = (
235
+ f"STORY (in {language}): {story_for_image}\n"
236
+ f"INSTRUCTIONS (in English): FORMAT: Create a comic strip with exactly 4 panels. "
237
+ f"Each panel should have at most one speech bubble, and no more than 3 speech bubbles in total. "
238
+ f"Each speech bubble should contain only 1 to 3 words, in perfect {language} orthography. "
239
+ f"The comic must visually and textually reflect the story above. "
240
+ f"Show a child of age {age}, gender {gender}, and the comfort character {comfort_character} together in a safe, public, and friendly environment. "
241
+ f"Do not include any other animals. Use the story as inspiration for the actions and words in the comic. "
242
+ f"Context: {template['title']}. Culture: {culture}. Illustration style: {vibe_desc}."
243
+ )
244
+ else:
245
+ image_prompt = (
246
+ f"STORY (in {language}): {story_for_image}\n"
247
+ f"INSTRUCTIONS (in English): FORMAT: This is NOT a comic. Do NOT use panels, speech bubbles, or any text. Only draw a single illustration. "
248
+ f"Create an illustration in the style '{vibe}': {vibe_desc}. "
249
+ f"Show a child of age {age}, gender {gender}, and the comfort character {comfort_character} together in a safe, public and friendly environment. "
250
+ f"Do not include any other animals. Context: {template['title']}. Culture: {culture}. "
251
+ f"Only visual elements, no text. Key features: 5 visual features of the style. "
252
+ f"TONE AND MOOD: Gentle, supportive, calming. Friendly characters."
253
+ )
254
+
255
+ # Generate image
256
+ img = client.images.generate(
257
+ model="dall-e-3",
258
+ prompt=image_prompt,
259
+ size="1024x1024",
260
+ response_format="b64_json"
261
+ )
262
+
263
+ # Decode and save image
264
+ image_bytes = base64.b64decode(img.data[0].b64_json)
265
+ image = Image.open(io.BytesIO(image_bytes))
266
+
267
+ # Save to generated_images directory
268
+ image_filename = f"{scenario_file}_{vibe}_{language}.png"
269
+ image_path = GENERATED_IMAGES_DIR / image_filename
270
+ image.save(image_path)
271
+
272
+ return str(image_path)
273
+
274
+
275
+ # Helper function to list available scenarios
276
+ @mcp.tool()
277
+ def list_scenarios() -> list[str]:
278
+ """
279
+ Lists all available scenario templates.
280
+
281
+ Returns:
282
+ List of scenario names
283
+ """
284
+ return [p.stem.replace("_", " ") for p in TEMPLATES_DIR.glob("*.json")]
uv.lock CHANGED
@@ -115,6 +115,27 @@ wheels = [
115
  { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
116
  ]
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  [[package]]
119
  name = "brotli"
120
  version = "1.2.0"
@@ -173,6 +194,15 @@ wheels = [
173
  { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
174
  ]
175
 
 
 
 
 
 
 
 
 
 
176
  [[package]]
177
  name = "certifi"
178
  version = "2025.11.12"
@@ -380,6 +410,7 @@ version = "0.0.1"
380
  source = { virtual = "." }
381
  dependencies = [
382
  { name = "fastapi" },
 
383
  { name = "gradio" },
384
  { name = "mcp" },
385
  { name = "openai" },
@@ -392,6 +423,7 @@ dependencies = [
392
  [package.metadata]
393
  requires-dist = [
394
  { name = "fastapi", specifier = ">=0.122.0" },
 
395
  { name = "gradio", specifier = ">=6.0.1" },
396
  { name = "mcp", specifier = ">=1.22.0" },
397
  { name = "openai", specifier = ">=2.8.1" },
@@ -466,6 +498,32 @@ wheels = [
466
  { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
467
  ]
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  [[package]]
470
  name = "distro"
471
  version = "1.9.0"
@@ -475,12 +533,52 @@ wheels = [
475
  { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
476
  ]
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  [[package]]
479
  name = "exceptiongroup"
480
  version = "1.3.1"
481
  source = { registry = "https://pypi.org/simple" }
482
  dependencies = [
483
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
484
  ]
485
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
486
  wheels = [
@@ -502,6 +600,32 @@ wheels = [
502
  { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" },
503
  ]
504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  [[package]]
506
  name = "ffmpy"
507
  version = "1.0.0"
@@ -823,6 +947,21 @@ wheels = [
823
  { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
824
  ]
825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  [[package]]
827
  name = "jsonschema-specifications"
828
  version = "2025.9.1"
@@ -1136,6 +1275,18 @@ wheels = [
1136
  { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
1137
  ]
1138
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  [[package]]
1140
  name = "orjson"
1141
  version = "3.11.4"
@@ -1288,6 +1439,24 @@ wheels = [
1288
  { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
1289
  ]
1290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  [[package]]
1292
  name = "pillow"
1293
  version = "12.0.0"
@@ -1386,6 +1555,50 @@ wheels = [
1386
  { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
1387
  ]
1388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1389
  [[package]]
1390
  name = "pycparser"
1391
  version = "2.23"
@@ -1410,6 +1623,11 @@ wheels = [
1410
  { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
1411
  ]
1412
 
 
 
 
 
 
1413
  [[package]]
1414
  name = "pydantic-core"
1415
  version = "2.41.5"
@@ -1574,6 +1792,15 @@ crypto = [
1574
  { name = "cryptography" },
1575
  ]
1576
 
 
 
 
 
 
 
 
 
 
1577
  [[package]]
1578
  name = "python-dateutil"
1579
  version = "2.9.0.post0"
@@ -1701,16 +1928,16 @@ wheels = [
1701
 
1702
  [[package]]
1703
  name = "referencing"
1704
- version = "0.37.0"
1705
  source = { registry = "https://pypi.org/simple" }
1706
  dependencies = [
1707
  { name = "attrs" },
1708
  { name = "rpds-py" },
1709
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
1710
  ]
1711
- sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
1712
  wheels = [
1713
- { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
1714
  ]
1715
 
1716
  [[package]]
@@ -1741,6 +1968,19 @@ wheels = [
1741
  { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
1742
  ]
1743
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1744
  [[package]]
1745
  name = "rpds-py"
1746
  version = "0.29.0"
@@ -1936,6 +2176,55 @@ wheels = [
1936
  { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
1937
  ]
1938
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1939
  [[package]]
1940
  name = "tomlkit"
1941
  version = "0.13.3"
@@ -2037,3 +2326,62 @@ sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef468
2037
  wheels = [
2038
  { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
2039
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
116
  ]
117
 
118
+ [[package]]
119
+ name = "authlib"
120
+ version = "1.6.5"
121
+ source = { registry = "https://pypi.org/simple" }
122
+ dependencies = [
123
+ { name = "cryptography" },
124
+ ]
125
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
126
+ wheels = [
127
+ { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
128
+ ]
129
+
130
+ [[package]]
131
+ name = "beartype"
132
+ version = "0.22.8"
133
+ source = { registry = "https://pypi.org/simple" }
134
+ sdist = { url = "https://files.pythonhosted.org/packages/8c/1d/794ae2acaa67c8b216d91d5919da2606c2bb14086849ffde7f5555f3a3a5/beartype-0.22.8.tar.gz", hash = "sha256:b19b21c9359722ee3f7cc433f063b3e13997b27ae8226551ea5062e621f61165", size = 1602262, upload-time = "2025-12-03T05:11:10.766Z" }
135
+ wheels = [
136
+ { url = "https://files.pythonhosted.org/packages/14/2a/fbcbf5a025d3e71ddafad7efd43e34ec4362f4d523c3c471b457148fb211/beartype-0.22.8-py3-none-any.whl", hash = "sha256:b832882d04e41a4097bab9f63e6992bc6de58c414ee84cba9b45b67314f5ab2e", size = 1331895, upload-time = "2025-12-03T05:11:08.373Z" },
137
+ ]
138
+
139
  [[package]]
140
  name = "brotli"
141
  version = "1.2.0"
 
194
  { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
195
  ]
196
 
197
+ [[package]]
198
+ name = "cachetools"
199
+ version = "6.2.2"
200
+ source = { registry = "https://pypi.org/simple" }
201
+ sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" }
202
+ wheels = [
203
+ { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" },
204
+ ]
205
+
206
  [[package]]
207
  name = "certifi"
208
  version = "2025.11.12"
 
410
  source = { virtual = "." }
411
  dependencies = [
412
  { name = "fastapi" },
413
+ { name = "fastmcp" },
414
  { name = "gradio" },
415
  { name = "mcp" },
416
  { name = "openai" },
 
423
  [package.metadata]
424
  requires-dist = [
425
  { name = "fastapi", specifier = ">=0.122.0" },
426
+ { name = "fastmcp", specifier = ">=2.13.2" },
427
  { name = "gradio", specifier = ">=6.0.1" },
428
  { name = "mcp", specifier = ">=1.22.0" },
429
  { name = "openai", specifier = ">=2.8.1" },
 
498
  { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
499
  ]
500
 
501
+ [[package]]
502
+ name = "cyclopts"
503
+ version = "4.3.0"
504
+ source = { registry = "https://pypi.org/simple" }
505
+ dependencies = [
506
+ { name = "attrs" },
507
+ { name = "docstring-parser" },
508
+ { name = "rich" },
509
+ { name = "rich-rst" },
510
+ { name = "tomli", marker = "python_full_version < '3.11'" },
511
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
512
+ ]
513
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/fe026df2ab8301e30a2b0bd425ff1462ad858fd4f991c1ac0389c2059c24/cyclopts-4.3.0.tar.gz", hash = "sha256:e95179cd0a959ce250ecfb2f0262a5996a92c1f9467bccad2f3d829e6833cef5", size = 151411, upload-time = "2025-11-25T02:59:33.572Z" }
514
+ wheels = [
515
+ { url = "https://files.pythonhosted.org/packages/7a/e8/77a231ae531cf38765b75ddf27dae28bb5f70b41d8bb4f15ce1650e93f57/cyclopts-4.3.0-py3-none-any.whl", hash = "sha256:91a30b69faf128ada7cfeaefd7d9649dc222e8b2a8697f1fc99e4ee7b7ca44f3", size = 187184, upload-time = "2025-11-25T02:59:32.21Z" },
516
+ ]
517
+
518
+ [[package]]
519
+ name = "diskcache"
520
+ version = "5.6.3"
521
+ source = { registry = "https://pypi.org/simple" }
522
+ sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
523
+ wheels = [
524
+ { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
525
+ ]
526
+
527
  [[package]]
528
  name = "distro"
529
  version = "1.9.0"
 
533
  { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
534
  ]
535
 
536
+ [[package]]
537
+ name = "dnspython"
538
+ version = "2.8.0"
539
+ source = { registry = "https://pypi.org/simple" }
540
+ sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
541
+ wheels = [
542
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
543
+ ]
544
+
545
+ [[package]]
546
+ name = "docstring-parser"
547
+ version = "0.17.0"
548
+ source = { registry = "https://pypi.org/simple" }
549
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
550
+ wheels = [
551
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
552
+ ]
553
+
554
+ [[package]]
555
+ name = "docutils"
556
+ version = "0.22.3"
557
+ source = { registry = "https://pypi.org/simple" }
558
+ sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" }
559
+ wheels = [
560
+ { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" },
561
+ ]
562
+
563
+ [[package]]
564
+ name = "email-validator"
565
+ version = "2.3.0"
566
+ source = { registry = "https://pypi.org/simple" }
567
+ dependencies = [
568
+ { name = "dnspython" },
569
+ { name = "idna" },
570
+ ]
571
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
572
+ wheels = [
573
+ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
574
+ ]
575
+
576
  [[package]]
577
  name = "exceptiongroup"
578
  version = "1.3.1"
579
  source = { registry = "https://pypi.org/simple" }
580
  dependencies = [
581
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
582
  ]
583
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
584
  wheels = [
 
600
  { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" },
601
  ]
602
 
603
+ [[package]]
604
+ name = "fastmcp"
605
+ version = "2.13.2"
606
+ source = { registry = "https://pypi.org/simple" }
607
+ dependencies = [
608
+ { name = "authlib" },
609
+ { name = "cyclopts" },
610
+ { name = "exceptiongroup" },
611
+ { name = "httpx" },
612
+ { name = "jsonschema-path" },
613
+ { name = "mcp" },
614
+ { name = "openapi-pydantic" },
615
+ { name = "platformdirs" },
616
+ { name = "py-key-value-aio", extra = ["disk", "memory"] },
617
+ { name = "pydantic", extra = ["email"] },
618
+ { name = "pyperclip" },
619
+ { name = "python-dotenv" },
620
+ { name = "rich" },
621
+ { name = "uvicorn" },
622
+ { name = "websockets" },
623
+ ]
624
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/7a/4c6375a56f7458a4a6af62f4c4838a2c957a665cf5edad26fe95395666f1/fastmcp-2.13.2.tar.gz", hash = "sha256:2a206401a6579fea621974162674beba85b467ad72c70c1a3752a31951dff7f0", size = 8185950, upload-time = "2025-12-01T18:48:16.834Z" }
625
+ wheels = [
626
+ { url = "https://files.pythonhosted.org/packages/e5/4b/73c68b0ae9e587f20c5aa13ba5bed9be2bb9248a598555dafcf17df87f70/fastmcp-2.13.2-py3-none-any.whl", hash = "sha256:300c59eb970c235bb9d0575883322922e4f2e2468a3d45e90cbfd6b23b7be245", size = 385643, upload-time = "2025-12-01T18:48:18.515Z" },
627
+ ]
628
+
629
  [[package]]
630
  name = "ffmpy"
631
  version = "1.0.0"
 
947
  { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
948
  ]
949
 
950
+ [[package]]
951
+ name = "jsonschema-path"
952
+ version = "0.3.4"
953
+ source = { registry = "https://pypi.org/simple" }
954
+ dependencies = [
955
+ { name = "pathable" },
956
+ { name = "pyyaml" },
957
+ { name = "referencing" },
958
+ { name = "requests" },
959
+ ]
960
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
961
+ wheels = [
962
+ { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" },
963
+ ]
964
+
965
  [[package]]
966
  name = "jsonschema-specifications"
967
  version = "2025.9.1"
 
1275
  { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
1276
  ]
1277
 
1278
+ [[package]]
1279
+ name = "openapi-pydantic"
1280
+ version = "0.5.1"
1281
+ source = { registry = "https://pypi.org/simple" }
1282
+ dependencies = [
1283
+ { name = "pydantic" },
1284
+ ]
1285
+ sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
1286
+ wheels = [
1287
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
1288
+ ]
1289
+
1290
  [[package]]
1291
  name = "orjson"
1292
  version = "3.11.4"
 
1439
  { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
1440
  ]
1441
 
1442
+ [[package]]
1443
+ name = "pathable"
1444
+ version = "0.4.4"
1445
+ source = { registry = "https://pypi.org/simple" }
1446
+ sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" }
1447
+ wheels = [
1448
+ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
1449
+ ]
1450
+
1451
+ [[package]]
1452
+ name = "pathvalidate"
1453
+ version = "3.3.1"
1454
+ source = { registry = "https://pypi.org/simple" }
1455
+ sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
1456
+ wheels = [
1457
+ { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
1458
+ ]
1459
+
1460
  [[package]]
1461
  name = "pillow"
1462
  version = "12.0.0"
 
1555
  { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
1556
  ]
1557
 
1558
+ [[package]]
1559
+ name = "platformdirs"
1560
+ version = "4.5.0"
1561
+ source = { registry = "https://pypi.org/simple" }
1562
+ sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
1563
+ wheels = [
1564
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
1565
+ ]
1566
+
1567
+ [[package]]
1568
+ name = "py-key-value-aio"
1569
+ version = "0.3.0"
1570
+ source = { registry = "https://pypi.org/simple" }
1571
+ dependencies = [
1572
+ { name = "beartype" },
1573
+ { name = "py-key-value-shared" },
1574
+ ]
1575
+ sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" }
1576
+ wheels = [
1577
+ { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" },
1578
+ ]
1579
+
1580
+ [package.optional-dependencies]
1581
+ disk = [
1582
+ { name = "diskcache" },
1583
+ { name = "pathvalidate" },
1584
+ ]
1585
+ memory = [
1586
+ { name = "cachetools" },
1587
+ ]
1588
+
1589
+ [[package]]
1590
+ name = "py-key-value-shared"
1591
+ version = "0.3.0"
1592
+ source = { registry = "https://pypi.org/simple" }
1593
+ dependencies = [
1594
+ { name = "beartype" },
1595
+ { name = "typing-extensions" },
1596
+ ]
1597
+ sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" }
1598
+ wheels = [
1599
+ { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" },
1600
+ ]
1601
+
1602
  [[package]]
1603
  name = "pycparser"
1604
  version = "2.23"
 
1623
  { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
1624
  ]
1625
 
1626
+ [package.optional-dependencies]
1627
+ email = [
1628
+ { name = "email-validator" },
1629
+ ]
1630
+
1631
  [[package]]
1632
  name = "pydantic-core"
1633
  version = "2.41.5"
 
1792
  { name = "cryptography" },
1793
  ]
1794
 
1795
+ [[package]]
1796
+ name = "pyperclip"
1797
+ version = "1.11.0"
1798
+ source = { registry = "https://pypi.org/simple" }
1799
+ sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
1800
+ wheels = [
1801
+ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
1802
+ ]
1803
+
1804
  [[package]]
1805
  name = "python-dateutil"
1806
  version = "2.9.0.post0"
 
1928
 
1929
  [[package]]
1930
  name = "referencing"
1931
+ version = "0.36.2"
1932
  source = { registry = "https://pypi.org/simple" }
1933
  dependencies = [
1934
  { name = "attrs" },
1935
  { name = "rpds-py" },
1936
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
1937
  ]
1938
+ sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
1939
  wheels = [
1940
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
1941
  ]
1942
 
1943
  [[package]]
 
1968
  { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
1969
  ]
1970
 
1971
+ [[package]]
1972
+ name = "rich-rst"
1973
+ version = "1.3.2"
1974
+ source = { registry = "https://pypi.org/simple" }
1975
+ dependencies = [
1976
+ { name = "docutils" },
1977
+ { name = "rich" },
1978
+ ]
1979
+ sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
1980
+ wheels = [
1981
+ { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
1982
+ ]
1983
+
1984
  [[package]]
1985
  name = "rpds-py"
1986
  version = "0.29.0"
 
2176
  { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
2177
  ]
2178
 
2179
+ [[package]]
2180
+ name = "tomli"
2181
+ version = "2.3.0"
2182
+ source = { registry = "https://pypi.org/simple" }
2183
+ sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
2184
+ wheels = [
2185
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
2186
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
2187
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
2188
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
2189
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
2190
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
2191
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
2192
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
2193
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
2194
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
2195
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
2196
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
2197
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
2198
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
2199
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
2200
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
2201
+ { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
2202
+ { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
2203
+ { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
2204
+ { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
2205
+ { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
2206
+ { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
2207
+ { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
2208
+ { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
2209
+ { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
2210
+ { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
2211
+ { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
2212
+ { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
2213
+ { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
2214
+ { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
2215
+ { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
2216
+ { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
2217
+ { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
2218
+ { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
2219
+ { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
2220
+ { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
2221
+ { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
2222
+ { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
2223
+ { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
2224
+ { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
2225
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
2226
+ ]
2227
+
2228
  [[package]]
2229
  name = "tomlkit"
2230
  version = "0.13.3"
 
2326
  wheels = [
2327
  { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
2328
  ]
2329
+
2330
+ [[package]]
2331
+ name = "websockets"
2332
+ version = "15.0.1"
2333
+ source = { registry = "https://pypi.org/simple" }
2334
+ sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
2335
+ wheels = [
2336
+ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
2337
+ { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
2338
+ { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
2339
+ { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
2340
+ { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
2341
+ { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
2342
+ { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
2343
+ { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
2344
+ { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
2345
+ { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
2346
+ { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
2347
+ { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
2348
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
2349
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
2350
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
2351
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
2352
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
2353
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
2354
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
2355
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
2356
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
2357
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
2358
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
2359
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
2360
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
2361
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
2362
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
2363
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
2364
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
2365
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
2366
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
2367
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
2368
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
2369
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
2370
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
2371
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
2372
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
2373
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
2374
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
2375
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
2376
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
2377
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
2378
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
2379
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
2380
+ { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
2381
+ { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
2382
+ { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
2383
+ { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
2384
+ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
2385
+ { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
2386
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
2387
+ ]