├── .gitignore ├── LICENSE ├── README.md ├── beemcp ├── __init__.py ├── bee.py ├── beemcp.py ├── conversation.py ├── fact.py ├── location.py ├── manual_testing.py ├── todo.py └── utils.py ├── claude-chat-screenshot.png ├── claude-desktop-configuration.png ├── claude_desktop_config.example.json ├── extended-chat-screenshot.png ├── pyproject.toml ├── suggested_fact.png └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .python-version 12 | 13 | # Sensitive files 14 | .env 15 | .env.* 16 | *.pem 17 | credentials.json 18 | config.local.json 19 | 20 | # IDE and editor files 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # OS specific files 28 | .DS_Store 29 | Thumbs.db 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Roger Pincombe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BeeMCP - A Bee MCP Server 2 | 3 | [![PyPI version](https://badge.fury.io/py/beemcp.svg?cache=1)](https://badge.fury.io/py/beemcp) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | **Unofficial** Model Context Protocol (MCP) server for interacting with your [Bee wearable](https://bee.computer/) lifelogging data. More context [on my blog](https://rogerpincombe.com/beemcp). 7 | 8 | This server acts as a bridge, allowing Large Language Models (LLMs) like Claude or custom AI agents to access and interact with your personal data stored in Bee, including conversations, facts, to-dos, and location history. 9 | 10 | **Disclaimer:** This is an unofficial project and is not affiliated with Bee. Use it at your own risk. Ensure you understand the security implications of granting AI access to your personal data via the API key. 11 | 12 | ## Core Concept 13 | 14 | Bee.computer helps you capture moments from your life (conversations, places visited, notes). `beemcp` makes this data available to your AI assistant through the Model Context Protocol. This means you can ask your AI questions like: 15 | 16 | * "What important things did I discuss last week?" 17 | * "Remind me about Brad's dietary preferences." 18 | * "Where was I last Tuesday afternoon?" 19 | * "Add 'Book flight tickets' to my reminders." 20 | 21 | The AI, using `beemcp`, can securely fetch or modify this information from your Bee.computer account. 22 | 23 | ![Example Claude Desktop Chat](https://github.com/OkGoDoIt/beemcp/raw/master/claude-chat-screenshot.png) 24 | 25 | ## Prerequisites 26 | 27 | 1. **Python:** Version 3.10 or higher. 28 | 2. **Bee.computer API Key:** You need an [API key from your Bee account settings](https://developer.bee.computer/keys). 29 | 30 | ## Installation 31 | 32 | You can install and run `beemcp` using `uv` (recommended) or `pip`. 33 | 34 | ### Using uv (recommended) 35 | 36 | [uv](https://docs.astral.sh/uv/) is a fast Python package installer and resolver. If you have `uv` installed, you don't need to install `beemcp` separately. You can run it directly using [`uvx`](https://docs.astral.sh/uv/guides/tools/): 37 | 38 | ```bash 39 | # Example of running directly (requires API key configured, see below) 40 | uvx beemcp 41 | ``` 42 | 43 | ### Using PIP 44 | 45 | Alternatively, you can install `beemcp` using `pip`: 46 | 47 | ```bash 48 | pip install beemcp 49 | ``` 50 | 51 | After installation, you can run it as a Python module: 52 | 53 | ```bash 54 | python -m beemcp.beemcp 55 | ``` 56 | 57 | Or, if the entry point is correctly added to your system's PATH during installation, you might be able to run it directly: 58 | 59 | ```bash 60 | beemcp 61 | ``` 62 | 63 | ## Configuration 64 | 65 | ### 1. API Key Setup (required) 66 | 67 | `beemcp` requires your Bee API key to function. **Never share this key publicly or commit it to version control.** 68 | 69 | Get your API key from the [Bee developer website here](https://developer.bee.computer/keys). 70 | 71 | If running in Claude or another MCP client, you will likely provide the `BEE_API_TOKEN` environment variable in the client's configuration. 72 | 73 | If running directly from the command line, provide the key is using a `.env` file in the directory where you run the `beemcp` server: 74 | 75 | 1. Create a file named `.env` in the same directory you intend to run `beemcp` from. 76 | 2. Add the following line to the `.env` file, replacing `your_actual_bee_api_key_here` with your real key: 77 | 78 | ```dotenv 79 | BEE_API_TOKEN="your_actual_bee_api_key_here" 80 | ``` 81 | 82 | Alternatively, you can set the `BEE_API_TOKEN` environment variable directly in your system or shell: 83 | 84 | ```bash 85 | export BEE_API_TOKEN="your_actual_bee_api_key_here" 86 | # Now run the server in the same shell session 87 | uvx beemcp 88 | ``` 89 | 90 | **The server will exit with an error if the `BEE_API_TOKEN` is not found.** 91 | 92 | ### 2. Connecting to LLM Clients 93 | 94 | You need to tell your LLM client (like Claude.app or Zed) how to start and communicate with the `beemcp` server. 95 | 96 | #### Configure for Claude.app 97 | 98 | Add the following to your Claude settings (`settings.json`): 99 | 100 | >Using uvx (Recommended): 101 | 102 | ```json 103 | "mcpServers": { 104 | "beemcp": { 105 | "command": "uvx", 106 | "args": ["beemcp"], 107 | "env": {"BEE_API_TOKEN": ""} 108 | } 109 | } 110 | ``` 111 | 112 |
113 | Using pip installation 114 | 115 | ```json 116 | "mcpServers": { 117 | "bee": { 118 | "command": "python", 119 | "args": ["-m", "beemcp.beemcp"], 120 | "env": {"BEE_API_TOKEN": ""} 121 | } 122 | } 123 | ``` 124 |
125 | 126 | If you go to the `Settings` window in Claude Desktop and open the `Developer` tab, you should see something like this: 127 | 128 | ![Example Claude Desktop Configuration](https://github.com/OkGoDoIt/beemcp/raw/master/claude-desktop-configuration.png) 129 | 130 | #### Configure for Zed 131 | 132 | Add the following to your Zed `settings.json`: 133 | 134 |
135 | Using uvx (Recommended) 136 | 137 | ```json 138 | "context_servers": [ 139 | { 140 | "name": "beemcp", 141 | "command": "uvx", 142 | "args": ["beemcp"], 143 | "env": {"BEE_API_TOKEN": ""} 144 | } 145 | ], 146 | ``` 147 |
148 | 149 |
150 | Using pip installation 151 | 152 | ```json 153 | "context_servers": [ 154 | { 155 | "name": "beemcp", 156 | "command": "python", 157 | "args": ["-m", "beemcp.beemcp"], 158 | "env": {"BEE_API_TOKEN": ""} 159 | } 160 | ], 161 | ``` 162 |
163 | 164 | ## Available Tools 165 | 166 | These are the actions the LLM can request from `beemcp`. 167 | 168 | ### Conversations 169 | 170 | * `list-all-conversations` 171 | * **Description:** List all conversations the user has had including short summaries. Use this to get an overview of the user's conversation history for context. Then you can use the get-conversation tool to get the full details of a specific conversation. 172 | * `get-conversation` 173 | * **Description:** Get the full details of a conversation by its ID. Use this to retrieve specific conversation details when the user asks about a particular conversation. You can use the list-all-conversations tool to get the id of the conversation you want to look up. 174 | * **Arguments:** 175 | * `id` (integer): The ID of the conversation. 176 | 177 | ### Facts 178 | 179 | * `list-all-user-facts` 180 | * **Description:** List all facts the user has recorded. Use this to get an overview of the user's recorded facts for context. Then you can use the get-user-fact tool to get the full details of a specific fact. 181 | * `get-user-fact` 182 | * **Description:** Get the full details of a fact by its ID. Use this to retrieve specific fact details when the user asks about a particular fact. You can use the list-all-user-facts tool to get the id of the fact you want to look up. 183 | * **Arguments:** 184 | * `id` (integer): The ID of the fact. 185 | * `record-user-fact` 186 | * **Description:** Create a new fact. Use this to record new information about the user, their preferences, and other details that might be relevant to answering other questions. 187 | * **Arguments:** 188 | * `text` (string): The content of the fact. 189 | * `update-user-fact` 190 | * **Description:** Update an existing fact. Use this to update information about the user, their preferences, and other details that might be relevant to answering other questions. Set confirmed to true if the user has explicitly confirmed the fact, otherwise if it's just an implied fact gathered from context then set confirmed to false. 191 | * **Arguments:** 192 | * `id` (integer): The ID of the fact to update. 193 | * `text` (string): The new content for the fact. 194 | * `confirmed` (boolean): Whether the user has confirmed this fact. 195 | * `confirm-user-fact` 196 | * **Description:** Mark a fact as confirmed. Use this to update the confirmation status of a fact based on user feedback. Set confirmed to true if the user has explicitly confirmed the fact, otherwise if it's just an implied fact gathered from context then set confirmed to false. 197 | * **Arguments:** 198 | * `id` (integer): The ID of the fact to confirm/unconfirm. 199 | * `confirmed` (boolean): The new confirmation status. 200 | * `delete-user-fact` 201 | * **Description:** Delete an existing fact. Use this to forget information about the user, their preferences, and other details. Only call this if the user explicitly says to. 202 | * **Arguments:** 203 | * `id` (integer): The ID of the fact to delete. 204 | 205 | ### Todos (Reminders) 206 | 207 | * `list-all-todos` 208 | * **Description:** List all todos (reminders) the user has created. Use this to get a comprehensive view of all the user's tasks, both completed and pending. 209 | * `list-incomplete-todos` 210 | * **Description:** List all incomplete todos (reminders) the user still has to do. Use this proactively to see pending tasks that still need to be completed. 211 | * `create-todo` 212 | * **Description:** Create a new todo for the user. Set `alarm_at` to an ISO 8601 formatted date-time string if the todo has a specific deadline or reminder time. 213 | * **Arguments:** 214 | * `text` (string): The content of the todo. 215 | * `alarm_at` (string, optional): ISO 8601 datetime string (e.g., "2024-12-31T23:59:00Z"). 216 | * `update-todo` 217 | * **Description:** Update an existing todo. You can modify the text, completion status, or alarm time. Only include parameters you want to change. 218 | * **Arguments:** 219 | * `id` (integer): The ID of the todo to update. 220 | * `text` (string, optional): New text for the todo. 221 | * `completed` (boolean, optional): New completion status. 222 | * `alarm_at` (string, optional): New ISO 8601 alarm time. 223 | * `delete-todo` 224 | * **Description:** Delete an existing todo. Only call this if the user explicitly says to delete a todo. 225 | * **Arguments:** 226 | * `id` (integer): The ID of the todo to delete. 227 | * `mark-todo-completed` 228 | * **Description:** Mark a todo as completed. Call this when a user explicitly says they've completed a task. 229 | * **Arguments:** 230 | * `id` (integer): The ID of the todo to mark as complete. 231 | 232 | ### Locations 233 | 234 | Some of these convenience functions are redundant but are added to make usage with current large language models more practical 235 | 236 | * `list-all-locations` 237 | * **Description:** List all locations the user has recorded. Use this to get a comprehensive view of all the user's location history. 238 | * `get-locations-today` 239 | * **Description:** Get locations the user visited in the last 24 hours. Use this when the user asks about where they were today or in the last day. 240 | * `get-locations-week` 241 | * **Description:** Get locations the user visited in the last 7 days. Use this when the user asks about where they were this week or in the last few days. 242 | * `get-locations-month` 243 | * **Description:** Get locations the user visited in the last 30 days. Use this when the user asks about where they were this month or in the last few weeks. 244 | * `get-locations-by-time` 245 | * **Description:** Get locations within a specific time range. Use this when the user asks about where they were during a particular period. `start_time` and `end_time` should be ISO 8601 formatted date-time strings, in this format: 2025-12-31T00:00:00Z 246 | * **Arguments:** 247 | * `start_time` (string, optional): Start time in ISO 8601 format. 248 | * `end_time` (string, optional): End time in ISO 8601 format. 249 | 250 | ## Available Resources 251 | 252 | MCP Resources provide direct access to data, often used for context or caching by the LLM client. many LLm clients do not support resources very well, so the "Tools" listed above are provided even when they may be redundant. 253 | 254 | * `bee://conversations`: List summaries of all conversations. 255 | * `bee://conversations/{id}`: Get full details for a specific conversation. 256 | * `bee://facts`: List summaries of all confirmed facts. 257 | * `bee://facts/{id}`: Get full details for a specific fact. 258 | * `bee://todos`: List summaries of all todos. 259 | * `bee://todos/incomplete`: List summaries of incomplete todos. 260 | * `bee://todos/{id}`: Get full details for a specific todo. 261 | * `bee://locations`: List summaries of all recorded locations (combined sequentially). 262 | * `bee://locations/today`: List locations from the last 24 hours. 263 | * `bee://locations/week`: List locations from the last 7 days. 264 | * `bee://locations/month`: List locations from the last 30 days. 265 | 266 | ## Example Interactions 267 | 268 | ![Example Claude Desktop Chat](https://github.com/OkGoDoIt/beemcp/raw/master/claude-chat-screenshot.png) 269 | 270 | ![Example extended chat](https://github.com/OkGoDoIt/beemcp/raw/master/extended-chat-screenshot.png) 271 | 272 | After which the Bee app on your phone will suggest the fact: 273 | 274 | ![Example Bee app suggestion](https://github.com/OkGoDoIt/beemcp/raw/master/suggested_fact.png) 275 | 276 | ## Debugging 277 | 278 | You can use the MCP inspector tool (`@modelcontextprotocol/inspector`) to interact with and debug the `beemcp` server directly. 279 | 280 | If you installed using `uv` and are running with `uvx`: 281 | 282 | ```bash 283 | npx @modelcontextprotocol/inspector uvx beemcp 284 | ``` 285 | 286 | If you installed using `pip`: 287 | 288 | ```bash 289 | npx @modelcontextprotocol/inspector python -m beemcp.beemcp 290 | ``` 291 | 292 | If you are developing locally within the project directory: 293 | 294 | ```bash 295 | # Assuming you are in the root directory of the beemcp project 296 | npx @modelcontextprotocol/inspector python -m beemcp.beemcp 297 | # Or if using uv for development 298 | npx @modelcontextprotocol/inspector uv run beemcp.beemcp 299 | ``` 300 | 301 | ## Example Questions for your LLM 302 | 303 | Try asking your AI assistant questions like these to leverage `beemcp`: 304 | 305 | * "What conversations did I have yesterday?" 306 | * "Look up the conversation with John about the project." 307 | * "Remember that I like my coffee black." (-> `record-user-fact`) 308 | * "Actually, I take milk in my coffee." (-> `update-user-fact`) 309 | * "What's on my todo list?" 310 | * "Show me my incomplete tasks." 311 | * "Add 'Buy groceries' to my reminders." 312 | * "Mark the 'Send report' todo as done." 313 | * "Where did I go last weekend?" 314 | * "What places did I visit today?" 315 | 316 | ## License 317 | 318 | `beemcp` is licensed under the **MIT License**. You are free to use, modify, and distribute this software under the terms of the license. See the `LICENSE` file (or the standard MIT license text) for details. 319 | -------------------------------------------------------------------------------- /beemcp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkGoDoIt/beemcp/11e2d2a8bbfd5a366d3bc9e75e62d7b56bd17370/beemcp/__init__.py -------------------------------------------------------------------------------- /beemcp/bee.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | import requests 4 | from .fact import Fact, fact_from_dict 5 | from .conversation import Conversation, conversation_from_dict 6 | from .todo import Todo, todo_from_dict 7 | from .location import Location, combine_locations, location_from_dict 8 | 9 | class Bee: 10 | def __init__(self, api_key): 11 | self.api_key = api_key 12 | self.base_url = 'https://api.bee.computer' 13 | 14 | def get_conversations(self, user_id="me", page=1, limit=10) -> List[Conversation]: 15 | try: 16 | response = requests.get( 17 | f"{self.base_url}/v1/{user_id}/conversations", 18 | params={"page": page, "limit": limit}, 19 | headers={"x-api-key": self.api_key} 20 | ) 21 | response.raise_for_status() 22 | return conversation_from_dict(response.json()["conversations"]) 23 | except requests.exceptions.RequestException as e: 24 | raise Exception(f"Failed to get conversations: {str(e)}") 25 | 26 | def get_all_conversations(self, user_id="me") -> List[Conversation]: 27 | """ 28 | Retrieves all conversations by paginating through results. 29 | 30 | Args: 31 | user_id: The ID of the user whose conversations to retrieve 32 | limit: Number of conversations to retrieve per page 33 | 34 | Returns: 35 | A complete list of all conversations for the user 36 | """ 37 | all_conversations = [] 38 | current_page = 1 39 | 40 | while True: 41 | # Get the current page of conversations 42 | page_conversations = self.get_conversations(user_id=user_id, page=current_page, limit=250) 43 | 44 | # Add the conversations from this page to our complete list 45 | all_conversations.extend(page_conversations) 46 | 47 | # If we received less than 250 conversations, we've reached the end 48 | if not page_conversations or len(page_conversations) < 250: 49 | break 50 | 51 | # Move to the next page 52 | current_page += 1 53 | 54 | return all_conversations 55 | 56 | def get_conversation(self, conversation_id, user_id="me") -> Conversation: 57 | if conversation_id is None: 58 | return None 59 | try: 60 | response = requests.get( 61 | f"{self.base_url}/v1/{user_id}/conversations/{conversation_id}", 62 | headers={"x-api-key": self.api_key} 63 | ) 64 | response.raise_for_status() 65 | json = response.json() 66 | if "conversation" in json: 67 | return Conversation.from_dict(json["conversation"]) 68 | else: 69 | return Conversation.from_dict(json) 70 | except requests.exceptions.RequestException as e: 71 | raise Exception(f"Failed to get conversation: {str(e)}") 72 | 73 | def delete_conversation(self, conversation_id, user_id="me"): 74 | try: 75 | response = requests.delete( 76 | f"{self.base_url}/v1/{user_id}/conversations/{conversation_id}", 77 | headers={"x-api-key": self.api_key} 78 | ) 79 | response.raise_for_status() 80 | return response.json() 81 | except requests.exceptions.RequestException as e: 82 | raise Exception(f"Failed to delete conversation: {str(e)}") 83 | 84 | def end_conversation(self, conversation_id, user_id="me"): 85 | try: 86 | response = requests.post( 87 | f"{self.base_url}/v1/{user_id}/conversations/{conversation_id}/end", 88 | headers={"x-api-key": self.api_key} 89 | ) 90 | response.raise_for_status() 91 | return response.json() 92 | except requests.exceptions.RequestException as e: 93 | raise Exception(f"Failed to end conversation: {str(e)}") 94 | 95 | def retry_conversation(self, conversation_id, user_id="me"): 96 | try: 97 | response = requests.post( 98 | f"{self.base_url}/v1/{user_id}/conversations/{conversation_id}/retry", 99 | headers={"x-api-key": self.api_key} 100 | ) 101 | response.raise_for_status() 102 | return response.json() 103 | except requests.exceptions.RequestException as e: 104 | raise Exception(f"Failed to retry conversation: {str(e)}") 105 | 106 | def get_facts(self, user_id="me", page=1, limit=10, confirmed=True) -> list[Fact]: 107 | try: 108 | response = requests.get( 109 | f"{self.base_url}/v1/{user_id}/facts", 110 | params={"page": page, "limit": limit, "confirmed": confirmed}, 111 | headers={"x-api-key": self.api_key} 112 | ) 113 | response.raise_for_status() 114 | return fact_from_dict(response.json()["facts"]) 115 | except requests.exceptions.RequestException as e: 116 | raise Exception(f"Failed to get facts: {str(e)}") 117 | 118 | def get_all_facts(self, user_id="me", confirmed=True) -> list[Fact]: 119 | """ 120 | Retrieves all facts by paginating through results. 121 | 122 | Args: 123 | user_id: The ID of the user whose facts to retrieve 124 | confirmed: Whether to retrieve only confirmed facts 125 | 126 | Returns: 127 | A complete list of all facts for the user 128 | """ 129 | all_facts = [] 130 | current_page = 1 131 | 132 | while True: 133 | # Get the current page of facts 134 | page_facts_response = self.get_facts(user_id=user_id, page=current_page, limit=250, confirmed=confirmed) 135 | 136 | # Add the facts from this page to our complete list 137 | all_facts.extend(page_facts_response) 138 | 139 | # If we received less than 250 facts, we've reached the end 140 | if not page_facts_response or len(page_facts_response) < 250: 141 | break 142 | 143 | # Move to the next page 144 | current_page += 1 145 | 146 | return all_facts 147 | 148 | def get_fact(self, fact_id, user_id="me") -> Fact: 149 | try: 150 | response = requests.get( 151 | f"{self.base_url}/v1/{user_id}/facts/{fact_id}", 152 | headers={"x-api-key": self.api_key} 153 | ) 154 | response.raise_for_status() 155 | json = response.json() 156 | if "fact" in json: 157 | return Fact.from_dict(json["fact"]) 158 | else: 159 | return Fact.from_dict(json) 160 | except requests.exceptions.RequestException as e: 161 | raise Exception(f"Failed to get fact: {str(e)}") 162 | 163 | def create_fact(self, text, confirmed=False, user_id="me") -> Fact: 164 | """ 165 | Creates a new fact for a user. 166 | 167 | Args: 168 | text: The text content of the fact 169 | user_id: The ID of the user to create the fact for 170 | 171 | Returns: 172 | The created fact as a dictionary 173 | """ 174 | try: 175 | response = requests.post( 176 | f"{self.base_url}/v1/{user_id}/facts", 177 | json={"text": text, "confirmed": confirmed}, 178 | headers={"x-api-key": self.api_key} 179 | ) 180 | response.raise_for_status() 181 | new_fact = Fact.from_dict(response.json()) 182 | if not confirmed: 183 | new_fact = self.update_fact(new_fact.id, confirmed=False) 184 | return new_fact 185 | except requests.exceptions.RequestException as e: 186 | raise Exception(f"Failed to create fact: {str(e)}") 187 | 188 | def update_fact(self, fact_id, text=None, confirmed=None, user_id="me") -> Fact: 189 | """ 190 | Updates an existing fact for a user. 191 | 192 | Args: 193 | fact_id: The ID of the fact to update 194 | text: The updated text content of the fact (optional) 195 | confirmed: The updated confirmation status of the fact (optional) 196 | user_id: The ID of the user who owns the fact 197 | 198 | Returns: 199 | The updated fact as a dictionary 200 | """ 201 | try: 202 | # Only include properties that were specified 203 | payload = {} 204 | if text is not None: 205 | payload["text"] = text 206 | if confirmed is not None: 207 | payload["confirmed"] = confirmed 208 | 209 | response = requests.put( 210 | f"{self.base_url}/v1/{user_id}/facts/{fact_id}", 211 | json=payload, 212 | headers={"x-api-key": self.api_key} 213 | ) 214 | response.raise_for_status() 215 | json = response.json() 216 | if "fact" in json: 217 | return Fact.from_dict(json["fact"]) 218 | else: 219 | return Fact.from_dict(json) 220 | except requests.exceptions.RequestException as e: 221 | raise Exception(f"Failed to update fact: {str(e)}") 222 | 223 | def delete_fact(self, fact_id, user_id="me") -> bool: 224 | try: 225 | response = requests.delete( 226 | f"{self.base_url}/v1/{user_id}/facts/{fact_id}", 227 | headers={"x-api-key": self.api_key} 228 | ) 229 | response.raise_for_status() 230 | json = response.json() 231 | if "success" in json: 232 | return json["success"] 233 | else: 234 | return False 235 | except requests.exceptions.RequestException as e: 236 | raise Exception(f"Failed to delete fact: {str(e)}") 237 | 238 | def get_locations(self, user_id="me", page=1, limit=10, conversation_id=None): 239 | try: 240 | params = {"page": page, "limit": limit} 241 | if conversation_id: 242 | params["conversationId"] = conversation_id 243 | response = requests.get( 244 | f"{self.base_url}/v1/{user_id}/locations", 245 | params=params, 246 | headers={"x-api-key": self.api_key} 247 | ) 248 | response.raise_for_status() 249 | return response.json() 250 | except requests.exceptions.RequestException as e: 251 | raise Exception(f"Failed to get locations: {str(e)}") 252 | 253 | def list_locations(self, user_id="me", page=1, limit=10, conversation_id=None) -> List[Location]: 254 | """ 255 | Retrieves a page of locations for a user. 256 | 257 | Args: 258 | user_id: The ID of the user whose locations to retrieve 259 | page: The page number to retrieve 260 | limit: Number of locations to retrieve per page 261 | conversation_id: Optional filter for locations specific to a conversation 262 | 263 | Returns: 264 | A list of Location objects for the requested page 265 | """ 266 | try: 267 | response = self.get_locations(user_id=user_id, page=page, limit=limit, conversation_id=conversation_id) 268 | return location_from_dict(response["locations"]) 269 | except Exception as e: 270 | raise Exception(f"Failed to list locations: {str(e)}") 271 | 272 | def list_all_locations(self, user_id="me", conversation_id=None) -> List[Location]: 273 | """ 274 | Retrieves all locations by paginating through results. 275 | 276 | Args: 277 | user_id: The ID of the user whose locations to retrieve 278 | conversation_id: Optional filter for locations specific to a conversation 279 | 280 | Returns: 281 | A complete list of all locations for the user 282 | """ 283 | all_locations = [] 284 | current_page = 1 285 | 286 | while True: 287 | # Get the current page of locations 288 | response = self.get_locations(user_id=user_id, page=current_page, limit=250, conversation_id=conversation_id) 289 | page_locations = location_from_dict(response["locations"]) 290 | 291 | # Add the locations from this page to our complete list 292 | all_locations.extend(page_locations) 293 | 294 | # If we received less than 250 locations, we've reached the end 295 | if not page_locations or len(page_locations) < 250: 296 | break 297 | 298 | # Move to the next page 299 | current_page += 1 300 | 301 | return combine_locations(all_locations) 302 | 303 | def get_locations_by_time_range(self, start_time=None, end_time=None, user_id="me", conversation_id=None) -> List[Location]: 304 | """ 305 | Retrieves locations within a specified time range. 306 | 307 | Args: 308 | start_time: The start time in ISO 8601 format (e.g., "2023-01-01T00:00:00Z") 309 | end_time: The end time in ISO 8601 format (e.g., "2023-01-31T23:59:59Z") 310 | user_id: The ID of the user whose locations to retrieve 311 | conversation_id: Optional filter for locations specific to a conversation 312 | 313 | Returns: 314 | A list of Location objects within the specified time range 315 | """ 316 | from datetime import datetime 317 | 318 | # Get all locations first 319 | all_locations = self.list_all_locations(user_id=user_id, conversation_id=conversation_id) 320 | 321 | 322 | if start_time and isinstance(start_time, str): 323 | start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00')) 324 | 325 | if end_time and isinstance(end_time, str): 326 | end_time = datetime.fromisoformat(end_time.replace('Z', '+00:00')) 327 | 328 | # Filter locations based on time range 329 | filtered_locations = [] 330 | for location in all_locations: 331 | # If start_time specified, skip locations before that time 332 | # Ensure both datetimes have the same timezone awareness for comparison 333 | location_time = location.start_time or location.created_at 334 | 335 | # If start_time specified, skip locations before that time 336 | if start_time: 337 | # Convert location_time to the same timezone awareness as start_time if needed 338 | if location_time.tzinfo is None and start_time.tzinfo is not None: 339 | # Make location_time timezone-aware with UTC 340 | from datetime import timezone 341 | location_time = location_time.replace(tzinfo=timezone.utc) 342 | elif location_time.tzinfo is not None and start_time.tzinfo is None: 343 | # Use naive comparison by removing timezone info 344 | location_time = location_time.replace(tzinfo=None) 345 | 346 | if location_time < start_time: 347 | continue 348 | 349 | # If end_time specified, skip locations after that time 350 | if end_time: 351 | # Reset location_time for end_time comparison 352 | location_time = location.end_time or location.created_at 353 | 354 | # Convert location_time to the same timezone awareness as end_time if needed 355 | if location_time.tzinfo is None and end_time.tzinfo is not None: 356 | # Make location_time timezone-aware with UTC 357 | from datetime import timezone 358 | location_time = location_time.replace(tzinfo=timezone.utc) 359 | elif location_time.tzinfo is not None and end_time.tzinfo is None: 360 | # Use naive comparison by removing timezone info 361 | location_time = location_time.replace(tzinfo=None) 362 | 363 | if location_time > end_time: 364 | continue 365 | 366 | filtered_locations.append(location) 367 | 368 | return filtered_locations 369 | 370 | def get_todos(self, user_id="me", page=1, limit=10) -> List[Todo]: 371 | """ 372 | Retrieves a page of todos for a user. 373 | 374 | Args: 375 | user_id: The ID of the user whose todos to retrieve 376 | page: The page number to retrieve 377 | limit: Number of todos to retrieve per page 378 | 379 | Returns: 380 | A list of Todo objects for the requested page 381 | """ 382 | try: 383 | response = requests.get( 384 | f"{self.base_url}/v1/{user_id}/todos", 385 | params={"page": page, "limit": limit}, 386 | headers={"x-api-key": self.api_key} 387 | ) 388 | response.raise_for_status() 389 | return todo_from_dict(response.json()["todos"]) 390 | except requests.exceptions.RequestException as e: 391 | raise Exception(f"Failed to get todos: {str(e)}") 392 | 393 | def get_all_todos(self, user_id="me") -> List[Todo]: 394 | """ 395 | Retrieves all todos by paginating through results. 396 | 397 | Args: 398 | user_id: The ID of the user whose todos to retrieve 399 | 400 | Returns: 401 | A complete list of all todos for the user 402 | """ 403 | all_todos = [] 404 | current_page = 1 405 | 406 | while True: 407 | # Get the current page of todos 408 | page_todos = self.get_todos(user_id=user_id, page=current_page, limit=250) 409 | 410 | # Add the todos from this page to our complete list 411 | all_todos.extend(page_todos) 412 | 413 | # If we received less than 250 todos, we've reached the end 414 | if not page_todos or len(page_todos) < 250: 415 | break 416 | 417 | # Move to the next page 418 | current_page += 1 419 | 420 | return all_todos 421 | 422 | def get_todo(self, todo_id, user_id="me") -> Todo: 423 | """ 424 | Retrieves a specific todo by ID. 425 | 426 | Args: 427 | todo_id: The ID of the todo to retrieve 428 | user_id: The ID of the user who owns the todo 429 | 430 | Returns: 431 | The requested Todo object 432 | """ 433 | try: 434 | response = requests.get( 435 | f"{self.base_url}/v1/{user_id}/todos/{todo_id}", 436 | headers={"x-api-key": self.api_key} 437 | ) 438 | response.raise_for_status() 439 | json = response.json() 440 | if "todo" in json: 441 | return Todo.from_dict(json["todo"]) 442 | else: 443 | return Todo.from_dict(json) 444 | except requests.exceptions.RequestException as e: 445 | raise Exception(f"Failed to get todo: {str(e)}") 446 | 447 | def create_todo(self, text, alarm_at=None, user_id="me") -> Todo: 448 | """ 449 | Creates a new todo for a user. 450 | 451 | Args: 452 | text: The text content of the todo 453 | alarm_at: Optional alarm time in ISO 8601 format 454 | user_id: The ID of the user to create the todo for 455 | 456 | Returns: 457 | The created Todo object 458 | """ 459 | try: 460 | payload = {"text": text} 461 | if alarm_at is not None: 462 | payload["alarm_at"] = alarm_at 463 | 464 | response = requests.post( 465 | f"{self.base_url}/v1/{user_id}/todos", 466 | json=payload, 467 | headers={"x-api-key": self.api_key} 468 | ) 469 | response.raise_for_status() 470 | return Todo.from_dict(response.json()) 471 | except requests.exceptions.RequestException as e: 472 | raise Exception(f"Failed to create todo: {str(e)}") 473 | 474 | def update_todo(self, todo_id, text=None, completed=None, alarm_at=None, user_id="me") -> Todo: 475 | """ 476 | Updates an existing todo for a user. 477 | 478 | Args: 479 | todo_id: The ID of the todo to update 480 | text: The updated text content of the todo (optional) 481 | completed: The updated completion status of the todo (optional) 482 | alarm_at: The updated alarm time in ISO 8601 format (optional) 483 | user_id: The ID of the user who owns the todo 484 | 485 | Returns: 486 | The updated Todo object 487 | """ 488 | try: 489 | # Only include properties that were specified 490 | payload = {} 491 | if text is not None: 492 | payload["text"] = text 493 | if completed is not None: 494 | payload["completed"] = completed 495 | if alarm_at is not None: 496 | payload["alarm_at"] = alarm_at 497 | 498 | response = requests.put( 499 | f"{self.base_url}/v1/{user_id}/todos/{todo_id}", 500 | json=payload, 501 | headers={"x-api-key": self.api_key} 502 | ) 503 | response.raise_for_status() 504 | json = response.json() 505 | if "todo" in json: 506 | return Todo.from_dict(json["todo"]) 507 | else: 508 | return Todo.from_dict(json) 509 | except requests.exceptions.RequestException as e: 510 | raise Exception(f"Failed to update todo: {str(e)}") 511 | 512 | def delete_todo(self, todo_id, user_id="me") -> bool: 513 | """ 514 | Deletes a todo by ID. 515 | 516 | Args: 517 | todo_id: The ID of the todo to delete 518 | user_id: The ID of the user who owns the todo 519 | 520 | Returns: 521 | True if deletion was successful, False otherwise 522 | """ 523 | try: 524 | response = requests.delete( 525 | f"{self.base_url}/v1/{user_id}/todos/{todo_id}", 526 | headers={"x-api-key": self.api_key} 527 | ) 528 | response.raise_for_status() 529 | json = response.json() 530 | if "success" in json: 531 | return json["success"] 532 | else: 533 | return False 534 | except requests.exceptions.RequestException as e: 535 | raise Exception(f"Failed to delete todo: {str(e)}") 536 | -------------------------------------------------------------------------------- /beemcp/beemcp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from mcp.server.fastmcp import FastMCP 3 | from .bee import Bee 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | bee = None 8 | mcp = FastMCP("Bee", instructions="Bee is a lifelogging platform. The user uses Bee to record their conversations, location history, and facts about themselves. If the user ever wants to recall previous conversations, interactions they've had, locations they've been,or general facts about themselves or their life, use Bee to look it up. Or you can proactively use Bee to load information about the user that might be useful in answering other questions or just generally giving more context about the user. Feel free to look through the user's conversation history, facts, and todo list in order to understand them better.") 9 | 10 | @mcp.resource("bee://conversations", name="list-all-conversations", description="List all conversations the user has had including short summaries. Use this to get a list of all the conversations so you can look up specific ones when relevant. Proactively call this so you have additional context about the user.") 11 | def resource_list_conversations() -> list[str]: 12 | all_conversations = bee.get_all_conversations() 13 | return [conversation.get_llm_summary() for conversation in all_conversations] 14 | 15 | @mcp.resource("bee://conversations/{id}", name="get-conversation", description="Get the full details of a conversation, including extended summary and takeaways. Use this the resource_list_conversations resource to find the id of the conversation you want to look up.") 16 | def resource_get_conversation(id: int) -> str: 17 | return bee.get_conversation(id).get_llm_text() 18 | 19 | @mcp.tool(name="list-all-conversations", description="List all conversations the user has had including short summaries. Use this to get an overview of the user's conversation history for context. Then you can use the get-conversation tool to get the full details of a specific conversation.") 20 | def list_all_conversations() -> str: 21 | """ 22 | Retrieves all conversations for the user and returns their summaries. 23 | 24 | Returns: 25 | A formatted string containing all conversation summaries 26 | """ 27 | conversations = resource_list_conversations() 28 | 29 | if not conversations: 30 | return "No conversations found." 31 | 32 | # Format the conversations into a readable response 33 | result = f"Found {len(conversations)} conversations:\n\n" 34 | result += "\n\n".join(conversations) 35 | 36 | return result 37 | 38 | 39 | @mcp.tool(name="get-conversation", description="Get the full details of a conversation by its ID. Use this to retrieve specific conversation details when the user asks about a particular conversation. You can use the list-all-conversations tool to get the id of the conversation you want to look up.") 40 | def get_conversation(id: int) -> str: 41 | """ 42 | Retrieves a specific conversation by ID and returns its details. 43 | 44 | Args: 45 | id: The ID of the conversation to retrieve 46 | 47 | Returns: 48 | A formatted string containing the conversation details 49 | """ 50 | conversation = bee.get_conversation(id) 51 | return "Retrieved conversation: " + conversation.get_llm_text() 52 | 53 | 54 | @mcp.resource("bee://facts", name="list-all-user-facts", description="List all facts the user has recorded. Proactively call this so you have additional information about the user, their preferences, and other details that might be relevant to answering other questions.") 55 | def resource_list_facts() -> list[str]: 56 | all_facts = bee.get_all_facts() 57 | return [fact.get_llm_summary() for fact in all_facts] 58 | 59 | @mcp.tool(name="list-all-user-facts", description="List all facts the user has recorded. Use this to get an overview of the user's recorded facts for context. Then you can use the get-user-fact tool to get the full details of a specific fact.") 60 | def list_all_user_facts() -> list[str]: 61 | """ 62 | Retrieves all facts for the user and returns them in a readable format. 63 | 64 | Returns: 65 | A formatted string containing all user facts 66 | """ 67 | return resource_list_facts() 68 | 69 | @mcp.resource("bee://facts/{id}", name="get-user-fact", description="Get the full details of a fact. Use this the resource_list_facts resource to find the id of the fact you want to look up.") 70 | def resource_get_fact(id: int) -> str: 71 | return bee.get_fact(id).get_llm_summary() 72 | 73 | @mcp.tool(name="get-user-fact", description="Get the full details of a fact by its ID. Use this to retrieve specific fact details when the user asks about a particular fact. You can use the list-all-user-facts tool to get the id of the fact you want to look up.") 74 | def get_fact(id: int) -> str: 75 | """ 76 | Retrieves a specific fact by ID and returns its details. 77 | 78 | Args: 79 | id: The ID of the fact to retrieve 80 | 81 | Returns: 82 | A formatted string containing the fact details 83 | """ 84 | fact = bee.get_fact(id) 85 | return "Retrieved fact about the user: " + fact.get_llm_text() 86 | 87 | 88 | @mcp.tool(name="record-user-fact", description="Create a new fact. Use this to record new information about the user, their preferences, and other details that might be relevant to answering other questions.") 89 | def create_fact(text: str) -> str: 90 | return "Saved fact (unconfirmed): " + bee.create_fact(text, False).get_llm_text() + "\n\nYou may ask the user if they want to confirm the fact, in which case you should call the bee://confirm_fact resource with the id of the fact and the confirmation status." 91 | 92 | @mcp.tool(name="update-user-fact", description="Update an existing fact. Use this to update information about the user, their preferences, and other details that might be relevant to answering other questions. Set confirmed to true if the user has explicitly confirmed the fact, otherwise if it's just an implied fact gathered from context then set confirmed to false.") 93 | def update_fact(id: int, text: str, confirmed: bool) -> str: 94 | return "Updated fact: " + bee.update_fact(id, text, confirmed).get_llm_text() 95 | 96 | @mcp.tool(name="confirm-user-fact", description="Mark a fact as confirmed. Use this to update the confirmation status of a fact based on user feedback. Set confirmed to true if the user has explicitly confirmed the fact, otherwise if it's just an implied fact gathered from context then set confirmed to false.") 97 | def confirm_fact(id: int, confirmed: bool) -> str: 98 | # Update the fact with new confirmation status 99 | updated_fact = bee.update_fact(id, confirmed=confirmed) 100 | # Return a message indicating the action taken 101 | confirmation_status = "confirmed" if confirmed else "unconfirmed" 102 | return f"Fact ID {id} marked as {confirmation_status}: {updated_fact.get_llm_text()}" 103 | 104 | @mcp.tool(name="delete-user-fact", description="Delete an existing fact. Use this to forget information about the user, their preferences, and other details. Only call this if the user explicitly says to.") 105 | def delete_fact(id: int) -> str: 106 | return "Deleted/forgot fact: " + "Success" if bee.delete_fact(id) else "Failed" 107 | 108 | @mcp.resource("bee://todos", name="list-all-todos", description="List all todos the user has created. Use this to get a list of all the todos so you can look up specific ones when relevant.") 109 | def resource_list_todos() -> list[str]: 110 | all_todos = bee.get_all_todos() 111 | return [todo.get_llm_summary() for todo in all_todos] 112 | 113 | @mcp.tool(name="list-all-todos", description="List all todos (reminders) the user has created. Use this to get a comprehensive view of all the user's tasks, both completed and pending.") 114 | def list_all_todos() -> list[str]: 115 | """ 116 | Retrieves all todos for the user and returns them as a formatted string. 117 | 118 | Returns: 119 | A list of strings containing summaries of all todos 120 | """ 121 | return resource_list_todos() 122 | 123 | @mcp.tool(name="list-incomplete-todos", description="List all incomplete todos (reminders) the user still has to do. Use this proactively to see pending tasks that still need to be completed.") 124 | def list_incomplete_todos() -> list[str]: 125 | """ 126 | Retrieves all incomplete todos for the user and returns them as a formatted string. 127 | 128 | Returns: 129 | A list of strings containing summaries of all incomplete todos 130 | """ 131 | return resource_list_incomplete_todos() 132 | 133 | 134 | @mcp.resource("bee://todos/{id}", name="get-todo", description="Get the full details of a todo. Use this with the resource_list_todos resource to find the id of the todo you want to look up.") 135 | def resource_get_todo(id: int) -> str: 136 | return bee.get_todo(id).get_llm_text() 137 | 138 | 139 | @mcp.tool(name="create-todo", description="Create a new todo for the user. Set alarm_at to an ISO 8601 formatted date-time string if the todo has a specific deadline or reminder time.") 140 | def create_todo(text: str, alarm_at: str = None) -> str: 141 | new_todo = bee.create_todo(text, alarm_at) 142 | return "Created todo: " + new_todo.get_llm_text() 143 | 144 | @mcp.tool(name="update-todo", description="Update an existing todo. You can modify the text, completion status, or alarm time. Only include parameters you want to change.") 145 | def update_todo(id: int, text: str = None, completed: bool = None, alarm_at: str = None) -> str: 146 | updated_todo = bee.update_todo(id, text, completed, alarm_at) 147 | return "Updated todo: " + updated_todo.get_llm_text() 148 | 149 | @mcp.tool(name="delete-todo", description="Delete an existing todo. Only call this if the user explicitly says to delete a todo.") 150 | def delete_todo(id: int) -> str: 151 | return "Deleted todo: " + "Success" if bee.delete_todo(id) else "Failed" 152 | 153 | @mcp.tool(name="mark-todo-completed", description="Mark a todo as completed. Call this when a user explicitly says they've completed a task.") 154 | def mark_todo_completed(id: int) -> str: 155 | updated_todo = bee.update_todo(id, completed=True) 156 | return f"Todo marked as completed: {updated_todo.get_llm_text()}" 157 | 158 | @mcp.resource("bee://todos/incomplete", name="list-incomplete-todos", description="List incomplete/pending todos the user has created. Use this to focus on pending tasks that still need to be completed. Proactively call this so you have additional context about the user's tasks and priorities.") 159 | def resource_list_incomplete_todos() -> list[str]: 160 | all_todos = bee.get_all_todos() 161 | incomplete_todos = [todo for todo in all_todos if not todo.completed] 162 | return [todo.get_llm_summary() for todo in incomplete_todos] 163 | 164 | @mcp.resource("bee://locations", name="list-all-locations", description="List all locations the user has recorded. This provides access to the user's location history.") 165 | def resource_list_locations() -> list[str]: 166 | all_locations = bee.list_all_locations() 167 | return [location.get_llm_summary() for location in all_locations] 168 | 169 | @mcp.tool(name="list-all-locations", description="List all locations the user has recorded. Use this to get a comprehensive view of all the user's location history.") 170 | def list_all_locations() -> list[str]: 171 | """ 172 | Retrieves all locations for the user and returns them as a formatted list. 173 | 174 | Returns: 175 | A list of strings containing summaries of all locations 176 | """ 177 | return resource_list_locations() 178 | 179 | @mcp.resource("bee://locations/today", name="get-locations-today", description="Get locations the user visited in the last 24 hours.") 180 | def resource_locations_today() -> list[str]: 181 | """ 182 | Retrieves locations the user visited in the last 24 hours. 183 | 184 | Returns: 185 | A list of location summaries within the specified time range 186 | """ 187 | start_time = datetime.now() - timedelta(days=1) 188 | end_time = datetime.now() 189 | filtered_locations = bee.get_locations_by_time_range(start_time=start_time, end_time=end_time) 190 | return [location.get_llm_summary() for location in filtered_locations] 191 | 192 | @mcp.resource("bee://locations/week", name="get-locations-week", description="Get locations the user visited in the last 7 days.") 193 | def resource_locations_week() -> list[str]: 194 | """ 195 | Retrieves locations the user visited in the last 7 days. 196 | 197 | Returns: 198 | A list of location summaries within the specified time range 199 | """ 200 | start_time = datetime.now() - timedelta(days=7) 201 | end_time = datetime.now() 202 | filtered_locations = bee.get_locations_by_time_range(start_time=start_time, end_time=end_time) 203 | return [location.get_llm_summary() for location in filtered_locations] 204 | 205 | @mcp.resource("bee://locations/month", name="get-locations-month", description="Get locations the user visited in the last 30 days.") 206 | def resource_locations_month() -> list[str]: 207 | """ 208 | Retrieves locations the user visited in the last 30 days. 209 | 210 | Returns: 211 | A list of location summaries within the specified time range 212 | """ 213 | start_time = datetime.now() - timedelta(days=30) 214 | end_time = datetime.now() 215 | filtered_locations = bee.get_locations_by_time_range(start_time=start_time, end_time=end_time) 216 | return [location.get_llm_summary() for location in filtered_locations] 217 | 218 | 219 | 220 | @mcp.tool(name="get-locations-by-time", description="Get locations within a specific time range. Use this when the user asks about where they were during a particular period. start_time and end_time should be ISO 8601 formatted date-time strings, in this format: 2025-12-31T00:00:00Z") 221 | def get_locations_time_range(start_time: str = None, end_time: str = None) -> list[str]: 222 | """ 223 | Retrieves locations within a specified time range. 224 | 225 | Args: 226 | start_time: The start time in ISO 8601 format (e.g., "2024-01-01T00:00:00Z") 227 | end_time: The end time in ISO 8601 format (e.g., "2025-01-31T23:59:59Z") 228 | 229 | Returns: 230 | A list of location summaries within the specified time range 231 | """ 232 | start_time = datetime.fromisoformat(start_time) 233 | end_time = datetime.fromisoformat(end_time) 234 | filtered_locations = bee.get_locations_by_time_range(start_time=start_time, end_time=end_time) 235 | return [location.get_llm_summary() for location in filtered_locations] 236 | 237 | @mcp.tool(name="get-locations-today", description="Get locations the user visited in the last 24 hours. Use this when the user asks about where they were today or in the last day.") 238 | def tool_locations_today() -> list[str]: 239 | """ 240 | Retrieves locations the user visited in the last 24 hours. 241 | 242 | Returns: 243 | A list of location summaries within the specified time range 244 | """ 245 | # Reuse the existing resource function 246 | return resource_locations_today() 247 | 248 | @mcp.tool(name="get-locations-week", description="Get locations the user visited in the last 7 days. Use this when the user asks about where they were this week or in the last few days.") 249 | def tool_locations_week() -> list[str]: 250 | """ 251 | Retrieves locations the user visited in the last 7 days. 252 | 253 | Returns: 254 | A list of location summaries within the specified time range 255 | """ 256 | # Reuse the existing resource function 257 | return resource_locations_week() 258 | 259 | @mcp.tool(name="get-locations-month", description="Get locations the user visited in the last 30 days. Use this when the user asks about where they were this month or in the last few weeks.") 260 | def tool_locations_month() -> list[str]: 261 | """ 262 | Retrieves locations the user visited in the last 30 days. 263 | 264 | Returns: 265 | A list of location summaries within the specified time range 266 | """ 267 | # Reuse the existing resource function 268 | return resource_locations_month() 269 | 270 | # Setup code could go here if needed (e.g., argument parsing) 271 | # Load environment variables from .env file *if* present 272 | # This allows running directly or via installed script 273 | load_dotenv() 274 | 275 | # Get API token from environment variables - this check remains crucial 276 | api_token = os.getenv("BEE_API_TOKEN") 277 | if not api_token: 278 | print("Error: BEE_API_TOKEN environment variable is not set.") 279 | print("Please create a .env file with BEE_API_TOKEN='your_token' or set the environment variable.") 280 | import sys 281 | sys.exit(1) # Exit if token is missing 282 | 283 | # Initialize Bee with API token 284 | bee = Bee(api_token) 285 | 286 | def main(): 287 | print("Starting BeeMCP server...") 288 | mcp.run() 289 | 290 | if __name__ == '__main__': 291 | main() -------------------------------------------------------------------------------- /beemcp/conversation.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Any, Optional, List, TypeVar, Type, cast, Callable 4 | from .utils import from_none, from_str, from_float, from_datetime, from_union, relative_time_range, to_class, from_int, from_list, to_float 5 | from .location import Location 6 | 7 | @dataclass 8 | class Conversation: 9 | id: int 10 | start_time: datetime 11 | end_time: datetime 12 | device_type: str 13 | summary: str 14 | short_summary: str 15 | state: str 16 | created_at: datetime 17 | updated_at: datetime 18 | primary_location: Optional[Location] = None 19 | 20 | @staticmethod 21 | def from_dict(obj: Any) -> 'Conversation': 22 | assert isinstance(obj, dict) 23 | id = from_int(obj.get("id")) 24 | start_time = from_datetime(obj.get("start_time")) 25 | end_time = from_datetime(obj.get("end_time")) 26 | device_type = from_str(obj.get("device_type")) 27 | summary = from_str(obj.get("summary")) 28 | short_summary = from_str(obj.get("short_summary")) 29 | state = from_str(obj.get("state")) 30 | created_at = from_datetime(obj.get("created_at")) 31 | updated_at = from_datetime(obj.get("updated_at")) 32 | primary_location = from_union([Location.from_dict, from_none], obj.get("primary_location")) 33 | return Conversation(id, start_time, end_time, device_type, summary, short_summary, state, created_at, updated_at, primary_location) 34 | 35 | def to_dict(self) -> dict: 36 | result: dict = {} 37 | result["id"] = from_int(self.id) 38 | result["start_time"] = self.start_time.isoformat() 39 | result["end_time"] = self.end_time.isoformat() 40 | result["device_type"] = from_str(self.device_type) 41 | result["summary"] = from_str(self.summary) 42 | result["short_summary"] = from_str(self.short_summary) 43 | result["state"] = from_str(self.state) 44 | result["created_at"] = self.created_at.isoformat() 45 | result["updated_at"] = self.updated_at.isoformat() 46 | result["primary_location"] = from_union([lambda x: to_class(Location, x), from_none], self.primary_location) 47 | return result 48 | 49 | def get_llm_text(self) -> str: 50 | text = f""" 51 | Conversation URI: bee://conversations/{self.id} 52 | Time: {relative_time_range(self.start_time, self.end_time)}""" 53 | if self.primary_location and self.primary_location.address: 54 | text += f"\nPrimary Location: {self.primary_location.address or 'Unknown'}" 55 | if self.short_summary: 56 | text += f"\nShort Summary: {self.short_summary.strip()}" 57 | if self.summary: 58 | fixed_summary = self.summary 59 | if fixed_summary.startswith("**Summary**\n"): 60 | fixed_summary = fixed_summary[13:] 61 | fixed_summary = fixed_summary.replace("Summary: ", "").strip() 62 | fixed_summary = fixed_summary.replace("**Summary**", "").strip() 63 | fixed_summary = fixed_summary.replace("## Summary\n", "").strip() 64 | fixed_summary = fixed_summary.replace("\n\n## Atmosphere\n", "\nAtmosphere: ").strip() 65 | fixed_summary = fixed_summary.replace("\n\nAtmosphere\n", "\nAtmosphere: ").strip() 66 | fixed_summary = fixed_summary.replace("\n\n## Key Take Aways\n", "\nKey Take Aways: ").strip() 67 | fixed_summary = fixed_summary.replace("\n\nKey Take Aways\n", "\nKey Take Aways: ").strip() 68 | fixed_summary = fixed_summary.replace("\n\n## Key Takeaways\n", "\nKey Takeaways: ").strip() 69 | fixed_summary = fixed_summary.replace("\n\nKey Takeaways\n", "\nKey Takeaways: ").strip() 70 | text += f"\nSummary: {fixed_summary.strip()}" 71 | text += "" 72 | return text 73 | 74 | def get_llm_summary(self) -> str: 75 | text = f""" 76 | Time: {relative_time_range(self.start_time, self.end_time)}""" 77 | if self.short_summary: 78 | text += f"\nShort Summary: {self.short_summary.strip()}" 79 | text += "" 80 | return text 81 | 82 | 83 | def conversation_from_dict(s: Any) -> List[Conversation]: 84 | return from_list(Conversation.from_dict, s) 85 | -------------------------------------------------------------------------------- /beemcp/fact.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Any, Optional, List, TypeVar, Type, cast, Callable 4 | from .utils import from_none, from_str, from_float, from_datetime, from_union, relative_time, to_class, from_int, from_list, to_float, from_bool 5 | 6 | @dataclass 7 | class Fact: 8 | id: int 9 | text: str 10 | tags: List[str] 11 | created_at: datetime 12 | visibility: str 13 | confirmed: Optional[bool] = None 14 | 15 | @staticmethod 16 | def from_dict(obj: Any) -> 'Fact': 17 | assert isinstance(obj, dict) 18 | id = from_int(obj.get("id")) 19 | text = from_str(obj.get("text")) 20 | tags = from_list(from_str, obj.get("tags")) 21 | created_at = from_datetime(obj.get("created_at")) 22 | visibility = from_str(obj.get("visibility")) 23 | confirmed = from_bool(obj.get("confirmed")) if "confirmed" in obj else None 24 | return Fact(id, text, tags, created_at, visibility, confirmed) 25 | 26 | def to_dict(self) -> dict: 27 | result: dict = {} 28 | result["id"] = from_int(self.id) 29 | result["text"] = from_str(self.text) 30 | result["tags"] = from_list(from_str, self.tags) 31 | result["created_at"] = self.created_at.isoformat() 32 | result["visibility"] = from_str(self.visibility) 33 | result["confirmed"] = from_bool(self.confirmed) if self.confirmed is not None else None 34 | return result 35 | 36 | def get_llm_text(self) -> str: 37 | text = f""" 38 | Recorded at (UTC): {self.created_at} 39 | Tags: {', '.join(self.tags)}""" 40 | if self.confirmed is False: 41 | text += f"\nTHIS IS UNCONFIRMED (it may be implied, but take it with a grain of salt)" 42 | elif self.confirmed is True: 43 | text += f"\n(This fact has been confirmed by the user)" 44 | if self.text: 45 | text += f"\nContent: {self.text.strip()}" 46 | text += "" 47 | return text 48 | 49 | def get_llm_summary(self) -> str: 50 | confirmed_text = " confirmed=\"true\"" if self.confirmed else "" 51 | return f"{self.text.strip()}" 52 | 53 | 54 | def fact_from_dict(s: Any) -> List[Fact]: 55 | return from_list(Fact.from_dict, s) 56 | 57 | -------------------------------------------------------------------------------- /beemcp/location.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Any, Optional, List, TypeVar, Type, cast, Callable 4 | from .utils import from_none, from_str, from_float, from_datetime, from_union, relative_time, relative_time_range, simple_time, simple_time_range, to_class, from_int, from_list, to_float 5 | 6 | @dataclass 7 | class Location: 8 | address: str 9 | latitude: float 10 | longitude: float 11 | created_at: datetime 12 | start_time: Optional[datetime] = None 13 | end_time: Optional[datetime] = None 14 | id: int = None 15 | 16 | @staticmethod 17 | def from_dict(obj: Any) -> 'Location': 18 | assert isinstance(obj, dict) 19 | address = from_str(obj.get("address")).strip(", ") 20 | latitude = from_float(obj.get("latitude")) 21 | longitude = from_float(obj.get("longitude")) 22 | created_at = from_datetime(obj.get("created_at")) 23 | start_time = None 24 | end_time = None 25 | id = from_int(obj.get("id")) if "id" in obj else None 26 | return Location(address, latitude, longitude, created_at, start_time, end_time, id) 27 | 28 | def to_dict(self) -> dict: 29 | result: dict = {} 30 | result["address"] = from_str(self.address) 31 | result["latitude"] = to_float(self.latitude) 32 | result["longitude"] = to_float(self.longitude) 33 | result["created_at"] = self.created_at.isoformat() 34 | if self.start_time is not None: 35 | result["start_time"] = self.start_time.isoformat() 36 | if self.end_time is not None: 37 | result["end_time"] = self.end_time.isoformat() 38 | if self.id is not None: 39 | result["id"] = from_int(self.id) 40 | return result 41 | 42 | def get_llm_text(self) -> str: 43 | id_attr = f" id=\"{self.id}\"" if self.id is not None else "" 44 | text = f"" 45 | if self.address: 46 | text += f"\nAddress: {self.address}" 47 | if self.latitude: 48 | text += f"\nLatitude: {self.latitude}" 49 | if self.longitude: 50 | text += f"\nLongitude: {self.longitude}" 51 | if self.start_time or self.end_time: 52 | text += f"\nTime: {simple_time_range(self.start_time, self.end_time)}" 53 | text += "" 54 | return text 55 | 56 | def get_llm_summary(self) -> str: 57 | id_attr = f" id=\"{self.id}\"" if self.id is not None else "" 58 | if self.start_time or self.end_time: 59 | text = f"" 60 | else: 61 | text = f"" 62 | if self.address: 63 | text += f"{self.address}" 64 | elif self.latitude and self.longitude: 65 | text += f"lat: {self.latitude}, long: {self.longitude}" 66 | text += "" 67 | return text 68 | 69 | def location_from_dict(s: Any) -> List[Location]: 70 | """ 71 | Convert a JSON dictionary to a list of Location objects. 72 | 73 | Args: 74 | s: JSON dictionary to convert 75 | 76 | Returns: 77 | List of Location objects 78 | """ 79 | return from_list(Location.from_dict, s) 80 | 81 | def combine_locations(locations: List[Location]) -> List[Location]: 82 | """ 83 | Combine matching sequential locations into a single location. 84 | 85 | Args: 86 | locations: List of Location objects 87 | 88 | Returns: 89 | List of combined Location objects 90 | """ 91 | combined_locations = [] 92 | # Return empty list if no locations provided 93 | if not locations: 94 | return [] 95 | 96 | # Sort locations by created_at timestamp 97 | sorted_locations = sorted(locations, key=lambda loc: loc.created_at) 98 | 99 | current_location = sorted_locations[0] 100 | for location in sorted_locations[1:]: 101 | if location.address == current_location.address or (location.latitude == current_location.latitude and location.longitude == current_location.longitude): 102 | if not current_location.start_time: 103 | current_location.start_time = current_location.created_at 104 | current_location.end_time = location.end_time or location.created_at 105 | else: 106 | combined_locations.append(current_location) 107 | current_location = location 108 | 109 | return combined_locations 110 | -------------------------------------------------------------------------------- /beemcp/manual_testing.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from .bee import Bee 3 | import os 4 | from dotenv import load_dotenv 5 | from .beemcp import resource_get_todo, create_todo, update_todo, delete_todo, mark_todo_completed, resource_list_incomplete_todos, resource_get_fact, resource_list_facts, resource_list_conversations, confirm_fact, get_fact, delete_fact, create_fact, resource_get_conversation, resource_get_fact, resource_list_facts, resource_list_conversations, list_all_locations, get_locations_time_range, resource_locations_today, resource_locations_week 6 | 7 | # Load environment variables from .env file 8 | load_dotenv() 9 | 10 | # Get API token from environment variables 11 | api_token = os.getenv("BEE_API_TOKEN") 12 | if not api_token: 13 | raise ValueError("BEE_API_TOKEN environment variable is not set") 14 | 15 | # Initialize Bee with API token 16 | bee = Bee(api_token) 17 | 18 | 19 | 20 | if __name__ == '__main__': 21 | print("=== TESTING CONVERSATIONS ===") 22 | conversations = resource_list_conversations() 23 | for conversation in conversations: 24 | if len(conversation) > 200: 25 | print(conversation[:200] + "...") 26 | else: 27 | print(conversation) 28 | conversation_id = input("Enter the id of the conversation to get (or press Enter to skip): ") 29 | if conversation_id: 30 | print(resource_get_conversation(conversation_id)) 31 | 32 | print("=== TESTING FACTS ===") 33 | facts = resource_list_facts() 34 | for fact in facts: 35 | print(fact) 36 | fact_id = input("Enter the id of the fact to get (or press Enter to skip): ") 37 | if fact_id: 38 | print(resource_get_fact(fact_id)) 39 | print("We will make that fact into a suggestion, so you can go back in the Bee app and approve it again.") 40 | print(confirm_fact(fact_id, False)) 41 | print(get_fact(fact_id)) 42 | new_fact = input("Type a new fact to create (or press Enter to skip): ") 43 | if new_fact: 44 | print(create_fact(new_fact)) 45 | 46 | print("=== TESTING TODOS ===") 47 | todos = resource_list_incomplete_todos() 48 | for todo in todos: 49 | print(todo) 50 | todo_id = input("Enter the id of the todo to update/delete (or press Enter to skip): ") 51 | if todo_id: 52 | print(resource_get_todo(todo_id)) 53 | # Set alarm time to one hour from now 54 | one_hour_from_now = datetime.now() + timedelta(hours=1) 55 | print(update_todo(todo_id, alarm_at=one_hour_from_now.isoformat())) 56 | print(resource_get_todo(todo_id)) 57 | print(mark_todo_completed(todo_id)) 58 | print(delete_todo(todo_id)) 59 | 60 | print("=== TESTING LOCATIONS ===") 61 | 62 | # Test listing all locations 63 | print("All locations:") 64 | all_locations = list_all_locations() 65 | for location in all_locations[:5]: # Show first 5 to avoid overwhelming output 66 | print(location) 67 | print(f"Total locations: {len(all_locations)}") 68 | 69 | # Test locations from today 70 | print("\nLocations from today:") 71 | today_locations = resource_locations_today() 72 | for location in today_locations: 73 | print(location) 74 | print(f"Total today locations: {len(today_locations)}") 75 | 76 | # Test locations from this week 77 | print("\nLocations from this week:") 78 | week_locations = resource_locations_week() 79 | print(f"Total week locations: {len(week_locations)}") 80 | for location in week_locations[:5]: # Show first 5 to avoid overwhelming output 81 | print(location) 82 | 83 | # Test custom date range 84 | print("\nTesting custom date range:") 85 | # Set date range to last 3 days 86 | end_date = datetime.now() 87 | start_date = end_date - timedelta(days=3) 88 | 89 | # Format dates as ISO strings 90 | start_iso = start_date.isoformat() 91 | end_iso = end_date.isoformat() 92 | 93 | print(f"Locations from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}:") 94 | custom_locations = get_locations_time_range(start_time=start_iso, end_time=end_iso) 95 | print(f"Total custom range locations: {len(custom_locations)}") 96 | for location in custom_locations[:5]: # Show first 5 to avoid overwhelming output 97 | print(location) 98 | 99 | # Get a specific location if available 100 | all_locations = bee.list_all_locations() 101 | location_id = input("Enter the ID of a location to view details (or press Enter to skip): ") 102 | if location_id: 103 | # Find the location with the given ID 104 | for location in all_locations: 105 | if str(location.id) == location_id: 106 | print(location.get_llm_text()) 107 | break 108 | else: 109 | print(f"Location with ID {location_id} not found") 110 | 111 | print("=== COMPLETE ===") 112 | -------------------------------------------------------------------------------- /beemcp/todo.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Any, Optional, List, TypeVar, Type, cast, Callable 4 | from .utils import from_none, from_str, from_float, from_datetime, from_union, relative_time, to_class, from_int, from_list, to_float, from_bool 5 | 6 | @dataclass 7 | class Todo: 8 | id: int 9 | text: str 10 | completed: bool 11 | created_at: datetime 12 | alarm_at: Optional[datetime] 13 | 14 | @staticmethod 15 | def from_dict(obj: Any) -> 'Todo': 16 | assert isinstance(obj, dict) 17 | id = from_int(obj.get("id")) 18 | text = from_str(obj.get("text")) 19 | completed = from_bool(obj.get("completed")) 20 | created_at = from_datetime(obj.get("created_at")) 21 | alarm_at = from_datetime(obj.get("alarm_at")) if "alarm_at" in obj else None 22 | return Todo(id, text, completed, created_at, alarm_at) 23 | 24 | def to_dict(self) -> dict: 25 | result: dict = {} 26 | result["id"] = from_int(self.id) 27 | result["text"] = from_str(self.text) 28 | result["completed"] = from_bool(self.completed) 29 | result["created_at"] = self.created_at.isoformat() 30 | if self.alarm_at: 31 | result["alarm_at"] = self.alarm_at.isoformat() 32 | return result 33 | 34 | def get_llm_text(self) -> str: 35 | text = f""" 36 | Created At (UTC): {self.created_at}""" 37 | if self.alarm_at: 38 | text += f"\nAlarm At (UTC): {self.alarm_at}" 39 | text += f"\nStatus: {'Completed' if self.completed else 'Incomplete'}" 40 | if self.text: 41 | text += f"\nText: {self.text.strip()}" 42 | text += "" 43 | return text 44 | 45 | def get_llm_summary(self) -> str: 46 | summary = f"" 47 | if len(self.text) > 150: 48 | summary += f"{self.text[:150].strip()}..." 49 | else: 50 | summary += f"{self.text.strip()}" 51 | summary += "" 52 | return summary 53 | 54 | def todo_from_dict(s: Any) -> List[Todo]: 55 | return from_list(Todo.from_dict, s) 56 | 57 | -------------------------------------------------------------------------------- /beemcp/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Optional, List, TypeVar, Type, cast, Callable 3 | 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | def from_str(x: Any) -> str: 9 | assert isinstance(x, str) 10 | return x 11 | 12 | 13 | def from_float(x: Any) -> float: 14 | assert isinstance(x, (float, int)) and not isinstance(x, bool) 15 | return float(x) 16 | 17 | 18 | def from_datetime(x: Any) -> datetime: 19 | # Parse datetime string in ISO format (e.g., "2025-03-18T18:32:12.178Z") 20 | # for standard ISO format timestamps 21 | if isinstance(x, str): 22 | if x.endswith('Z'): 23 | # Convert UTC 'Z' timezone marker to +00:00 format that fromisoformat understands 24 | x = x[:-1] + '+00:00' 25 | return datetime.fromisoformat(x) 26 | return x 27 | 28 | 29 | def to_float(x: Any) -> float: 30 | assert isinstance(x, (int, float)) 31 | return x 32 | 33 | 34 | def from_int(x: Any) -> int: 35 | assert isinstance(x, int) and not isinstance(x, bool) 36 | return x 37 | 38 | def from_bool(x: Any) -> bool: 39 | assert isinstance(x, bool) 40 | return x 41 | 42 | def to_bool(x: Any) -> bool: 43 | assert isinstance(x, bool) 44 | return x 45 | 46 | 47 | def from_none(x: Any) -> Any: 48 | assert x is None 49 | return x 50 | 51 | 52 | def from_union(fs, x): 53 | for f in fs: 54 | try: 55 | return f(x) 56 | except: 57 | pass 58 | assert False 59 | 60 | 61 | def to_class(c: Type[T], x: Any) -> dict: 62 | assert isinstance(x, c) 63 | return cast(Any, x).to_dict() 64 | 65 | 66 | def from_list(f: Callable[[Any], T], x: Any) -> List[T]: 67 | if isinstance(x, list): 68 | return [f(y) for y in x] 69 | else: 70 | return [f(x)] 71 | 72 | def simple_time_range(start: datetime, end: datetime) -> str: 73 | if start and end: 74 | if relative_time(start) == relative_time(end): 75 | return f"{simple_time(start)} - {simple_time(end)} ({relative_time_range(start, end, delta_only=True)} long, {relative_time(start)})" 76 | else: 77 | return f"{simple_time(start)} ({relative_time(start)}) - {simple_time(end)} ({relative_time(end)}) ({relative_time_range(start, end, delta_only=True)} long)" 78 | elif start: 79 | return f"{simple_time(start)} ({relative_time(start)})" 80 | elif end: 81 | return f"{simple_time(end)} ({relative_time(end)})" 82 | else: 83 | return "" 84 | 85 | def relative_time_range(start: datetime, end: datetime, delta_only=False) -> str: 86 | delta = end - start 87 | start_text = relative_time(start) 88 | if delta.seconds > 60 * 60: 89 | delta_text = f"{delta.seconds // (60 * 60)} hours" 90 | elif delta.seconds > 60: 91 | delta_text = f"{delta.seconds // 60} minutes" 92 | else: 93 | delta_text = f"{delta.seconds} seconds" 94 | if delta_only: 95 | return delta_text 96 | return f"{start_text} ({delta_text} long)" 97 | 98 | def relative_time(x: datetime) -> str: 99 | """ 100 | Returns a human-readable relative time string (e.g., "2 days ago"). 101 | Handles both timezone-aware and timezone-naive datetime objects. 102 | 103 | Args: 104 | x: The datetime to compare against current time 105 | 106 | Returns: 107 | A string representing the relative time 108 | """ 109 | # Ensure both datetimes have the same timezone awareness 110 | now = datetime.now(x.tzinfo) if x.tzinfo else datetime.now() 111 | 112 | delta = now - x 113 | if delta.days > 365: 114 | return f"{delta.days // 365} years ago" 115 | elif delta.days > 30: 116 | return f"{delta.days // 30} months ago" 117 | elif delta.days > 7: 118 | return f"{delta.days // 7} weeks ago" 119 | elif delta.days > 1: 120 | return f"{delta.days} days ago" 121 | elif delta.days == 1: 122 | return "yesterday" 123 | elif delta.seconds >= 3600: # Use seconds instead of hours attribute 124 | return "today" 125 | else: 126 | return "recently" 127 | 128 | def simple_time(x: datetime) -> str: 129 | return x.strftime("%Y-%m-%d %H:%M (UTC)") 130 | -------------------------------------------------------------------------------- /claude-chat-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkGoDoIt/beemcp/11e2d2a8bbfd5a366d3bc9e75e62d7b56bd17370/claude-chat-screenshot.png -------------------------------------------------------------------------------- /claude-desktop-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkGoDoIt/beemcp/11e2d2a8bbfd5a366d3bc9e75e62d7b56bd17370/claude-desktop-configuration.png -------------------------------------------------------------------------------- /claude_desktop_config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "beemcp": { 4 | "command": "uvx", 5 | "args": [ 6 | "beemcp" 7 | ], 8 | "env": { 9 | "BEE_API_TOKEN": "" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /extended-chat-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkGoDoIt/beemcp/11e2d2a8bbfd5a366d3bc9e75e62d7b56bd17370/extended-chat-screenshot.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "beemcp" 7 | version = "0.1.0" 8 | description = "A Model Context Protocol (MCP) server for Bee.computer lifelogging data (unofficial)" 9 | readme = "README.md" 10 | license = "MIT" 11 | requires-python = ">=3.10" 12 | dependencies = [ 13 | "mcp[cli]>=1.4.0", 14 | "requests>=2.31.0", 15 | "python-dotenv>=1.0.0" 16 | ] 17 | authors = [ 18 | { name="Roger Pincombe", email="roger@pincombe.com" }, 19 | ] 20 | maintainers = [ 21 | { name="Roger Pincombe", email="roger@pincombe.com" }, 22 | ] 23 | keywords = ["mcp", "model context protocol", "bee.computer", "bee ai", "bee", "lifelogging", "ai", "llm", "chatbot"] 24 | classifiers = [ # Standard classifiers from https://pypi.org/classifiers/ 25 | "Development Status :: 4 - Beta", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: End Users/Desktop", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 33 | "Operating System :: OS Independent", 34 | ] 35 | 36 | # URLs for project links on PyPI 37 | [project.urls] 38 | Homepage = "https://github.com/okgodoit/beemcp" 39 | Repository = "https://github.com/okgodoit/beemcp" 40 | "Bug Tracker" = "https://github.com/okgodoit/beemcp/issues" 41 | 42 | [project.scripts] 43 | beemcp = "beemcp.beemcp:main" 44 | 45 | [tool.setuptools] 46 | packages = ["beemcp"] 47 | -------------------------------------------------------------------------------- /suggested_fact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkGoDoIt/beemcp/11e2d2a8bbfd5a366d3bc9e75e62d7b56bd17370/suggested_fact.png -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.10" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.9.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 22 | ] 23 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 24 | wheels = [ 25 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 26 | ] 27 | 28 | [[package]] 29 | name = "beemcp" 30 | version = "0.1.0" 31 | source = { editable = "." } 32 | dependencies = [ 33 | { name = "mcp", extra = ["cli"] }, 34 | { name = "python-dotenv" }, 35 | { name = "requests" }, 36 | ] 37 | 38 | [package.metadata] 39 | requires-dist = [ 40 | { name = "mcp", extras = ["cli"], specifier = ">=1.4.0" }, 41 | { name = "python-dotenv", specifier = ">=1.0.0" }, 42 | { name = "requests", specifier = ">=2.31.0" }, 43 | ] 44 | 45 | [[package]] 46 | name = "certifi" 47 | version = "2025.1.31" 48 | source = { registry = "https://pypi.org/simple" } 49 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 50 | wheels = [ 51 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 52 | ] 53 | 54 | [[package]] 55 | name = "charset-normalizer" 56 | version = "3.4.1" 57 | source = { registry = "https://pypi.org/simple" } 58 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 59 | wheels = [ 60 | { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, 61 | { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, 62 | { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, 63 | { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, 64 | { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, 65 | { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, 66 | { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, 67 | { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, 68 | { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, 69 | { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, 70 | { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, 71 | { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, 72 | { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, 73 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 74 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 75 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 76 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 77 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 78 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 79 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 80 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 81 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 82 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 83 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 84 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 85 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 86 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 87 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 88 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 89 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 90 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 91 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 92 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 93 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 94 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 95 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 96 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 97 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 98 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 99 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 100 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 101 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 102 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 103 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 104 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 105 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 106 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 107 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 108 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 109 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 110 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 111 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 112 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 113 | ] 114 | 115 | [[package]] 116 | name = "click" 117 | version = "8.1.8" 118 | source = { registry = "https://pypi.org/simple" } 119 | dependencies = [ 120 | { name = "colorama", marker = "sys_platform == 'win32'" }, 121 | ] 122 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 123 | wheels = [ 124 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 125 | ] 126 | 127 | [[package]] 128 | name = "colorama" 129 | version = "0.4.6" 130 | source = { registry = "https://pypi.org/simple" } 131 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 134 | ] 135 | 136 | [[package]] 137 | name = "exceptiongroup" 138 | version = "1.2.2" 139 | source = { registry = "https://pypi.org/simple" } 140 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 141 | wheels = [ 142 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 143 | ] 144 | 145 | [[package]] 146 | name = "h11" 147 | version = "0.14.0" 148 | source = { registry = "https://pypi.org/simple" } 149 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 152 | ] 153 | 154 | [[package]] 155 | name = "httpcore" 156 | version = "1.0.7" 157 | source = { registry = "https://pypi.org/simple" } 158 | dependencies = [ 159 | { name = "certifi" }, 160 | { name = "h11" }, 161 | ] 162 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 163 | wheels = [ 164 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 165 | ] 166 | 167 | [[package]] 168 | name = "httpx" 169 | version = "0.28.1" 170 | source = { registry = "https://pypi.org/simple" } 171 | dependencies = [ 172 | { name = "anyio" }, 173 | { name = "certifi" }, 174 | { name = "httpcore" }, 175 | { name = "idna" }, 176 | ] 177 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 178 | wheels = [ 179 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 180 | ] 181 | 182 | [[package]] 183 | name = "httpx-sse" 184 | version = "0.4.0" 185 | source = { registry = "https://pypi.org/simple" } 186 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 187 | wheels = [ 188 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 189 | ] 190 | 191 | [[package]] 192 | name = "idna" 193 | version = "3.10" 194 | source = { registry = "https://pypi.org/simple" } 195 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 196 | wheels = [ 197 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 198 | ] 199 | 200 | [[package]] 201 | name = "markdown-it-py" 202 | version = "3.0.0" 203 | source = { registry = "https://pypi.org/simple" } 204 | dependencies = [ 205 | { name = "mdurl" }, 206 | ] 207 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 208 | wheels = [ 209 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 210 | ] 211 | 212 | [[package]] 213 | name = "mcp" 214 | version = "1.6.0" 215 | source = { registry = "https://pypi.org/simple" } 216 | dependencies = [ 217 | { name = "anyio" }, 218 | { name = "httpx" }, 219 | { name = "httpx-sse" }, 220 | { name = "pydantic" }, 221 | { name = "pydantic-settings" }, 222 | { name = "sse-starlette" }, 223 | { name = "starlette" }, 224 | { name = "uvicorn" }, 225 | ] 226 | sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, 229 | ] 230 | 231 | [package.optional-dependencies] 232 | cli = [ 233 | { name = "python-dotenv" }, 234 | { name = "typer" }, 235 | ] 236 | 237 | [[package]] 238 | name = "mdurl" 239 | version = "0.1.2" 240 | source = { registry = "https://pypi.org/simple" } 241 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 242 | wheels = [ 243 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 244 | ] 245 | 246 | [[package]] 247 | name = "pydantic" 248 | version = "2.11.1" 249 | source = { registry = "https://pypi.org/simple" } 250 | dependencies = [ 251 | { name = "annotated-types" }, 252 | { name = "pydantic-core" }, 253 | { name = "typing-extensions" }, 254 | { name = "typing-inspection" }, 255 | ] 256 | sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 } 257 | wheels = [ 258 | { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 }, 259 | ] 260 | 261 | [[package]] 262 | name = "pydantic-core" 263 | version = "2.33.0" 264 | source = { registry = "https://pypi.org/simple" } 265 | dependencies = [ 266 | { name = "typing-extensions" }, 267 | ] 268 | sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 } 269 | wheels = [ 270 | { url = "https://files.pythonhosted.org/packages/29/43/0649ad07e66b36a3fb21442b425bd0348ac162c5e686b36471f363201535/pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e", size = 2042968 }, 271 | { url = "https://files.pythonhosted.org/packages/a0/a6/975fea4774a459e495cb4be288efd8b041ac756a0a763f0b976d0861334b/pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518", size = 1860347 }, 272 | { url = "https://files.pythonhosted.org/packages/aa/49/7858dadad305101a077ec4d0c606b6425a2b134ea8d858458a6d287fd871/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73", size = 1910060 }, 273 | { url = "https://files.pythonhosted.org/packages/8d/4f/6522527911d9c5fe6d76b084d8b388d5c84b09d113247b39f91937500b34/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207", size = 1997129 }, 274 | { url = "https://files.pythonhosted.org/packages/75/d0/06f396da053e3d73001ea4787e56b4d7132a87c0b5e2e15a041e808c35cd/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b", size = 2140389 }, 275 | { url = "https://files.pythonhosted.org/packages/f5/6b/b9ff5b69cd4ef007cf665463f3be2e481dc7eb26c4a55b2f57a94308c31a/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f", size = 2754237 }, 276 | { url = "https://files.pythonhosted.org/packages/53/80/b4879de375cdf3718d05fcb60c9aa1f119d28e261dafa51b6a69c78f7178/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5", size = 2007433 }, 277 | { url = "https://files.pythonhosted.org/packages/46/24/54054713dc0af98a94eab37e0f4294dfd5cd8f70b2ca9dcdccd15709fd7e/pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276", size = 2123980 }, 278 | { url = "https://files.pythonhosted.org/packages/3a/4c/257c1cb89e14cfa6e95ebcb91b308eb1dd2b348340ff76a6e6fcfa9969e1/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760", size = 2087433 }, 279 | { url = "https://files.pythonhosted.org/packages/0c/62/927df8a39ad78ef7b82c5446e01dec9bb0043e1ad71d8f426062f5f014db/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa", size = 2260242 }, 280 | { url = "https://files.pythonhosted.org/packages/74/f2/389414f7c77a100954e84d6f52a82bd1788ae69db72364376d8a73b38765/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c", size = 2258227 }, 281 | { url = "https://files.pythonhosted.org/packages/53/99/94516313e15d906a1264bb40faf24a01a4af4e2ca8a7c10dd173b6513c5a/pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025", size = 1925523 }, 282 | { url = "https://files.pythonhosted.org/packages/7d/67/cc789611c6035a0b71305a1ec6ba196256ced76eba8375f316f840a70456/pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc", size = 1951872 }, 283 | { url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 }, 284 | { url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 }, 285 | { url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 }, 286 | { url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 }, 287 | { url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 }, 288 | { url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 }, 289 | { url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 }, 290 | { url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 }, 291 | { url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 }, 292 | { url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 }, 293 | { url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 }, 294 | { url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 }, 295 | { url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 }, 296 | { url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 }, 297 | { url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 }, 298 | { url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 }, 299 | { url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 }, 300 | { url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 }, 301 | { url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 }, 302 | { url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 }, 303 | { url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 }, 304 | { url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 }, 305 | { url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 }, 306 | { url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 }, 307 | { url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 }, 308 | { url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 }, 309 | { url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 }, 310 | { url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 }, 311 | { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 }, 312 | { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 }, 313 | { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 }, 314 | { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 }, 315 | { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 }, 316 | { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 }, 317 | { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 }, 318 | { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 }, 319 | { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 }, 320 | { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 }, 321 | { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 }, 322 | { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 }, 323 | { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 }, 324 | { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 }, 325 | { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 }, 326 | { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 }, 327 | { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, 328 | { url = "https://files.pythonhosted.org/packages/44/77/85e173b715e1a277ce934f28d877d82492df13e564fa68a01c96f36a47ad/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50", size = 2040129 }, 329 | { url = "https://files.pythonhosted.org/packages/33/e7/33da5f8a94bbe2191cfcd15bd6d16ecd113e67da1b8c78d3cc3478112dab/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c", size = 1872656 }, 330 | { url = "https://files.pythonhosted.org/packages/b4/7a/9600f222bea840e5b9ba1f17c0acc79b669b24542a78c42c6a10712c0aae/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1", size = 1903731 }, 331 | { url = "https://files.pythonhosted.org/packages/81/d2/94c7ca4e24c5dcfb74df92e0836c189e9eb6814cf62d2f26a75ea0a906db/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b", size = 2083966 }, 332 | { url = "https://files.pythonhosted.org/packages/b8/74/a0259989d220e8865ed6866a6d40539e40fa8f507e587e35d2414cc081f8/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d", size = 2118951 }, 333 | { url = "https://files.pythonhosted.org/packages/13/4c/87405ed04d6d07597920b657f082a8e8e58bf3034178bb9044b4d57a91e2/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442", size = 2079632 }, 334 | { url = "https://files.pythonhosted.org/packages/5a/4c/bcb02970ef91d4cd6de7c6893101302637da456bc8b52c18ea0d047b55ce/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a", size = 2250541 }, 335 | { url = "https://files.pythonhosted.org/packages/a3/2b/dbe5450c4cd904be5da736dcc7f2357b828199e29e38de19fc81f988b288/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330", size = 2255685 }, 336 | { url = "https://files.pythonhosted.org/packages/ca/a6/ca1d35f695d81f639c5617fc9efb44caad21a9463383fa45364b3044175a/pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae", size = 2082395 }, 337 | { url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 }, 338 | { url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 }, 339 | { url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 }, 340 | { url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 }, 341 | { url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 }, 342 | { url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 }, 343 | { url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 }, 344 | { url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 }, 345 | { url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 }, 346 | ] 347 | 348 | [[package]] 349 | name = "pydantic-settings" 350 | version = "2.8.1" 351 | source = { registry = "https://pypi.org/simple" } 352 | dependencies = [ 353 | { name = "pydantic" }, 354 | { name = "python-dotenv" }, 355 | ] 356 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 357 | wheels = [ 358 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 359 | ] 360 | 361 | [[package]] 362 | name = "pygments" 363 | version = "2.19.1" 364 | source = { registry = "https://pypi.org/simple" } 365 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 366 | wheels = [ 367 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 368 | ] 369 | 370 | [[package]] 371 | name = "python-dotenv" 372 | version = "1.1.0" 373 | source = { registry = "https://pypi.org/simple" } 374 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 375 | wheels = [ 376 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 377 | ] 378 | 379 | [[package]] 380 | name = "requests" 381 | version = "2.32.3" 382 | source = { registry = "https://pypi.org/simple" } 383 | dependencies = [ 384 | { name = "certifi" }, 385 | { name = "charset-normalizer" }, 386 | { name = "idna" }, 387 | { name = "urllib3" }, 388 | ] 389 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 390 | wheels = [ 391 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 392 | ] 393 | 394 | [[package]] 395 | name = "rich" 396 | version = "14.0.0" 397 | source = { registry = "https://pypi.org/simple" } 398 | dependencies = [ 399 | { name = "markdown-it-py" }, 400 | { name = "pygments" }, 401 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 402 | ] 403 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } 404 | wheels = [ 405 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, 406 | ] 407 | 408 | [[package]] 409 | name = "shellingham" 410 | version = "1.5.4" 411 | source = { registry = "https://pypi.org/simple" } 412 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 413 | wheels = [ 414 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 415 | ] 416 | 417 | [[package]] 418 | name = "sniffio" 419 | version = "1.3.1" 420 | source = { registry = "https://pypi.org/simple" } 421 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 422 | wheels = [ 423 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 424 | ] 425 | 426 | [[package]] 427 | name = "sse-starlette" 428 | version = "2.2.1" 429 | source = { registry = "https://pypi.org/simple" } 430 | dependencies = [ 431 | { name = "anyio" }, 432 | { name = "starlette" }, 433 | ] 434 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 435 | wheels = [ 436 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 437 | ] 438 | 439 | [[package]] 440 | name = "starlette" 441 | version = "0.46.1" 442 | source = { registry = "https://pypi.org/simple" } 443 | dependencies = [ 444 | { name = "anyio" }, 445 | ] 446 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 447 | wheels = [ 448 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 449 | ] 450 | 451 | [[package]] 452 | name = "typer" 453 | version = "0.15.2" 454 | source = { registry = "https://pypi.org/simple" } 455 | dependencies = [ 456 | { name = "click" }, 457 | { name = "rich" }, 458 | { name = "shellingham" }, 459 | { name = "typing-extensions" }, 460 | ] 461 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 462 | wheels = [ 463 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 464 | ] 465 | 466 | [[package]] 467 | name = "typing-extensions" 468 | version = "4.13.0" 469 | source = { registry = "https://pypi.org/simple" } 470 | sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } 471 | wheels = [ 472 | { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, 473 | ] 474 | 475 | [[package]] 476 | name = "typing-inspection" 477 | version = "0.4.0" 478 | source = { registry = "https://pypi.org/simple" } 479 | dependencies = [ 480 | { name = "typing-extensions" }, 481 | ] 482 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } 483 | wheels = [ 484 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, 485 | ] 486 | 487 | [[package]] 488 | name = "urllib3" 489 | version = "2.3.0" 490 | source = { registry = "https://pypi.org/simple" } 491 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 492 | wheels = [ 493 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 494 | ] 495 | 496 | [[package]] 497 | name = "uvicorn" 498 | version = "0.34.0" 499 | source = { registry = "https://pypi.org/simple" } 500 | dependencies = [ 501 | { name = "click" }, 502 | { name = "h11" }, 503 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 504 | ] 505 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 506 | wheels = [ 507 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 508 | ] 509 | --------------------------------------------------------------------------------