├── .claude └── commands │ └── prime.md ├── .gitignore ├── .mcp.json ├── .python-version ├── README.md ├── ai_docs ├── mcp-server-git-repomix-output.xml └── self-pocket-pick-mcp-server.xml ├── images └── pocket-pick.png ├── pyproject.toml ├── specs ├── pocket-pick-v1.md ├── pocket_list_ids.md └── require-id-during-add-feature.md ├── src └── mcp_server_pocket_pick │ ├── __init__.py │ ├── __main__.py │ ├── modules │ ├── __init__.py │ ├── constants.py │ ├── data_types.py │ ├── functionality │ │ ├── __init__.py │ │ ├── add.py │ │ ├── add_file.py │ │ ├── backup.py │ │ ├── find.py │ │ ├── get.py │ │ ├── list.py │ │ ├── list_ids.py │ │ ├── list_tags.py │ │ ├── remove.py │ │ └── to_file_by_id.py │ └── init_db.py │ ├── py.typed │ ├── server.py │ └── tests │ ├── __init__.py │ ├── functionality │ ├── __init__.py │ ├── test_add.py │ ├── test_add_file.py │ ├── test_backup.py │ ├── test_find.py │ ├── test_list.py │ ├── test_list_ids.py │ ├── test_list_tags.py │ ├── test_remove_get.py │ └── test_to_file_by_id.py │ └── test_init_db.py └── uv.lock /.claude/commands/prime.md: -------------------------------------------------------------------------------- 1 | # Context Prime 2 | > Follow the instructions to understand the context of the project. 3 | 4 | ## Run the following command 5 | 6 | eza . --tree --git-ignore 7 | 8 | ## Read the following files 9 | > Read the files below and nothing else. 10 | 11 | README.md 12 | pyproject.toml 13 | src/server.py 14 | src/mcp_server_pocket_pick/modules/data_types.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Environments 55 | .env 56 | .venv 57 | env/ 58 | venv/ 59 | ENV/ 60 | env.bak/ 61 | venv.bak/ 62 | 63 | # mypy 64 | .mypy_cache/ 65 | .dmypy.json 66 | dmypy.json 67 | 68 | # Editors 69 | .vscode/ 70 | .idea/ 71 | *.swp 72 | *.swo 73 | 74 | 75 | ai_docs/paic-pkb-repomix-output.xml 76 | 77 | 78 | 79 | **/.claude/settings.local.json 80 | 81 | database.db -------------------------------------------------------------------------------- /.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "updated-pocket-pick": { 4 | "command": "uv", 5 | "args": [ 6 | "--directory", 7 | ".", 8 | "run", 9 | "mcp-server-pocket-pick", 10 | "--database", 11 | "./database.db" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pocket Pick (MCP Server) 2 | > See how we used AI Coding, Claude Code, and MCP to build this tool on the [@IndyDevDan youtube channel](https://youtu.be/d-SyGA0Avtw). 3 | 4 | As engineers we end up reusing ideas, patterns and code snippets all the time but keeping track of these snippets can be hard and remembering where you stored them can be even harder. What if the exact snippet or idea you were looking for was one prompt away? 5 | 6 | With Anthropic's new MCP (Model Context Protocol) and a minimal portable database layer - we can solve this problem. Pocket Pick is your personal engineering knowledge base that lets you quickly store ideas, patterns and code snippets and gives you a DEAD SIMPLE text or tag based searching to quickly find them in the future. 7 | 8 | Pocket Pick 9 | 10 | ## Features 11 | 12 | - **Personal Knowledge Base**: Store code snippets, information, and ideas 13 | - **Tag-Based Organization**: Add tags to categorize and filter your knowledge 14 | - **Flexible Search**: Find content using substring, full-text, glob, regex, or exact matching 15 | - **MCP Integration**: Seamlessly works with Claude and other MCP-compatible AI assistants 16 | - **SQLite Backend**: Fast, reliable, and portable database storage 17 | - **Command-Line Interface**: Easy to use from the terminal 18 | 19 | ## Installation 20 | 21 | Install [uv](https://docs.astral.sh/uv/getting-started/installation/) 22 | 23 | ```bash 24 | # Clone the repository 25 | git clone https://github.com/indydevdan/pocket-pick.git 26 | cd pocket-pick 27 | 28 | # Install dependencies 29 | uv sync 30 | ``` 31 | 32 | Usage from JSON format 33 | 34 | Default Database for Claude Code 35 | 36 | ```json 37 | { 38 | "command": "uv", 39 | "args": ["--directory", ".", "run", "mcp-server-pocket-pick"] 40 | } 41 | ``` 42 | 43 | Custom Database for Claude Code 44 | 45 | ```json 46 | { 47 | "command": "uv", 48 | "args": ["--directory", ".", "run", "mcp-server-pocket-pick", "--database", "./database.db"] 49 | } 50 | ``` 51 | 52 | ## Usage with Claude Code 53 | 54 | ### Using .mcp.json 55 | 56 | You can configure Pocket Pick in your project's `.mcp.json` file for easy integration with Claude Code: 57 | 58 | ```json 59 | { 60 | "servers": { 61 | "pocket-pick": { 62 | "command": "uv", 63 | "args": ["--directory", "/path/to/pocket-pick", "run", "mcp-server-pocket-pick"] 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | With custom database location: 70 | 71 | ```json 72 | { 73 | "servers": { 74 | "pocket-pick": { 75 | "command": "uv", 76 | "args": ["--directory", "/path/to/pocket-pick", "run", "mcp-server-pocket-pick", "--database", "./custom-database.db"] 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | Place this file in your project directory, and Claude Code will automatically detect and use the configured MCP servers when started in that directory. 83 | 84 | ```bash 85 | # Add the pocket-pick server to Claude Code (if you're in the directory) 86 | claude mcp add pocket-pick -- \ 87 | uv --directory . \ 88 | run mcp-server-pocket-pick 89 | 90 | # Add the pocket-pick server to Claude Code 91 | claude mcp add pocket-pick -- \ 92 | uv --directory /path/to/pocket-pick-codebase \ 93 | run mcp-server-pocket-pick 94 | 95 | # With custom database location 96 | claude mcp add pocket-pick -- \ 97 | uv --directory /path/to/pocket-pick-codebase \ 98 | run mcp-server-pocket-pick --database ./database.db 99 | 100 | # List existing MCP servers - Validate that the server is running 101 | claude mcp list 102 | 103 | # Start claude code 104 | claude 105 | ``` 106 | 107 | ## Pocket Pick MCP Tools 108 | 109 | The following MCP tools are available in Pocket Pick: 110 | 111 | | Tool | Description | 112 | | -------------------- | -------------------------------------------- | 113 | | `pocket_add` | Add a new item with a specified ID to your knowledge base | 114 | | `pocket_add_file` | Add a file's content with a specified ID to your knowledge base | 115 | | `pocket_find` | Find items by text and/or tags | 116 | | `pocket_list` | List all items, optionally filtered by tags | 117 | | `pocket_list_tags` | List all tags with their counts | 118 | | `pocket_remove` | Remove an item by ID | 119 | | `pocket_get` | Get a specific item by ID | 120 | | `pocket_backup` | Backup the database | 121 | | `pocket_to_file_by_id` | Write an item's content to a file by its ID (requires absolute path) | 122 | 123 | ## Using with Claude 124 | 125 | After setting up Pocket Pick as an MCP server for Claude Code, you can use it your conversations: 126 | 127 | ### Adding Items 128 | 129 | Add items directly 130 | 131 | ```bash 132 | Add "claude mcp list" as a pocket pick item with ID "claude-mcp-list". tags: mcp, claude, code 133 | ``` 134 | 135 | Add items from clipboard 136 | 137 | ```bash 138 | pbpaste and create a pocket pick item with ID "python-fib" and the following tags: python, algorithm, fibonacci 139 | ``` 140 | 141 | Add items from a file 142 | 143 | ```bash 144 | Add the contents of ~/Documents/code-snippets/fibonacci.py to pocket pick with ID "fib-algorithm" and tags: python, algorithm, fibonacci 145 | ``` 146 | 147 | ### Listing Items 148 | List all items or tags: 149 | 150 | ``` 151 | list all my pocket picks 152 | ``` 153 | 154 | ### Finding Items 155 | 156 | Search for items in your knowledge base with tags 157 | 158 | ``` 159 | List pocket pick items with python and mcp tags 160 | ``` 161 | 162 | Search for text with specific content 163 | 164 | ``` 165 | pocket pick find "python" 166 | ``` 167 | 168 | ### Get or Remove Items 169 | 170 | Get or remove specific items: 171 | 172 | ``` 173 | get the pocket pick item with ID 1234-5678-90ab-cdef 174 | remove the pocket pick item with ID 1234-5678-90ab-cdef 175 | ``` 176 | 177 | ### Export to File 178 | 179 | Export a pocket pick item's content to a file by its ID. This allows you to save code snippets directly to files, create executable scripts from stored knowledge, or share content with others: 180 | 181 | ``` 182 | export the pocket pick item with ID 1234-5678-90ab-cdef to /Users/username/Documents/exported-snippet.py 183 | ``` 184 | 185 | The tool requires an absolute file path and will automatically create any necessary parent directories if they don't exist. 186 | 187 | ### Backup 188 | 189 | ``` 190 | backup the pocket pick database to ~/Documents/pocket-pick-backup.db 191 | ``` 192 | 193 | ## ID Management 194 | 195 | When adding items to Pocket Pick, you must now provide a unique ID: 196 | 197 | - IDs must be unique across your database 198 | - Choose descriptive IDs that help you identify the content 199 | - If you attempt to add an item with an ID that already exists, you'll receive an error 200 | 201 | ### ID Scheme Recommendations 202 | 203 | - **Descriptive IDs**: Use meaningful names like `python-sort-algorithm` or `css-flexbox-cheatsheet` 204 | - **Namespaced IDs**: Use prefixes like `py-`, `js-`, `css-` to categorize items 205 | - **UUID-style IDs**: Continue using UUIDs if you prefer automatically generated unique identifiers 206 | 207 | ## Search Modes 208 | 209 | Pocket Pick supports various search modes: 210 | 211 | - **substr**: (Default) Simple substring matching 212 | - **fts**: Full-text search with powerful capabilities: 213 | - Regular word search: Matches all words in any order (e.g., "python programming" finds entries with both words) 214 | - Exact phrase search: Use quotes for exact phrase matching (e.g., `"python programming"` only finds entries with that exact phrase) 215 | - **glob**: SQLite glob pattern matching (e.g., "test*" matches entries starting with "test") 216 | - **regex**: Regular expression matching 217 | - **exact**: Exact string matching 218 | 219 | Example find commands: 220 | 221 | ``` 222 | Find items containing "pyt" using substring matching 223 | Find items containing "def fibonacci" using full text search 224 | Find items containing "test*" using glob pattern matching 225 | Find items containing "^start.*test.*$" using regular expression matching 226 | Find items containing "match exactly test" using exact string matching 227 | ``` 228 | 229 | ## Database Structure 230 | 231 | Pocket Pick uses a simple SQLite database with the following schema: 232 | 233 | ```sql 234 | CREATE TABLE POCKET_PICK ( 235 | id TEXT PRIMARY KEY, -- UUID identifier 236 | created TIMESTAMP NOT NULL, -- Creation timestamp 237 | text TEXT NOT NULL, -- Item content 238 | tags TEXT NOT NULL -- JSON array of tags 239 | ) 240 | ``` 241 | 242 | The database file is located at `~/.pocket_pick.db` by default. 243 | 244 | ## Development 245 | 246 | ### Running Tests 247 | 248 | ```bash 249 | # Run all tests 250 | uv run pytest 251 | 252 | # Run with verbose output 253 | uv run pytest -v 254 | ``` 255 | 256 | ### Running the Server Directly 257 | 258 | ```bash 259 | # Start the MCP server 260 | uv run mcp-server-pocket-pick 261 | 262 | # With verbose logging 263 | uv run mcp-server-pocket-pick -v 264 | 265 | # With custom database location 266 | uv run mcp-server-pocket-pick --database ./database.db 267 | ``` 268 | 269 | ## Other Useful MCP Servers 270 | 271 | ### Fetch 272 | 273 | ```bash 274 | claude mcp add http-fetch -- uvx mcp-server-fetch 275 | ``` 276 | 277 | --- 278 | 279 | Built with ❤️ by [IndyDevDan](https://www.youtube.com/@indydevdan) with [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview), and [Principled AI Coding](https://agenticengineer.com/principled-ai-coding) 280 | 281 | -------------------------------------------------------------------------------- /images/pocket-pick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disler/pocket-pick/d8243d35036cb4b3c934180fce30952bc4a73365/images/pocket-pick.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-server-pocket-pick" 3 | version = "0.1.0" 4 | description = "Your Personal Knowledge Base with MCP" 5 | readme = "README.md" 6 | authors = [ 7 | { name = "IndyDevDan", email = "agentic@indydevdan.com" } 8 | ] 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "click>=8.1.7", 12 | "mcp>=1.0.0", 13 | "pydantic>=2.0.0", 14 | ] 15 | 16 | [project.scripts] 17 | mcp-server-pocket-pick = "mcp_server_pocket_pick:main" 18 | 19 | [build-system] 20 | requires = ["hatchling"] 21 | build-backend = "hatchling.build" 22 | 23 | [tool.uv] 24 | dev-dependencies = ["pytest>=8.0.0"] 25 | -------------------------------------------------------------------------------- /specs/pocket-pick-v1.md: -------------------------------------------------------------------------------- 1 | # Pocket Pick - Your Personal Knowledge Base 2 | 3 | As engineers we end up reusing ideas, patterns and code snippets all the time but keeping track of these snippets can be hard and remembering where you stored them can be even harder. What if the exact snippet or idea you were looking for was one prompt away? 4 | 5 | With Anthropics new MCP (model context protocol) and a minimal portable database layer - we can solve this problem. Pocket Pick is your personal engineering knowledge base that lets you quickly store ideas, patterns and code snippets and gives you a DEAD SIMPLE text or tag based searching to quickly find them in the future. 6 | 7 | To implement this we'll... 8 | 1. Build the key sqlite functionality 9 | 2. Test the functionality with pytest 10 | 3. Expose the functionality via MCP server. 11 | 12 | ## SQLITE Database Structure 13 | 14 | ``` 15 | CREATE TABLE if not exists POCKET_PICK { 16 | id: str, 17 | created: datetime, 18 | text: str, 19 | tags: str[], 20 | } 21 | ``` 22 | 23 | ## Implementation Notes 24 | - DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" - place in constants.py 25 | - always force (auto update) tags to be lowercase, trim whitespace, and use dash instead of spaces or underscores. 26 | - mcp comands will return whatever the command returns. 27 | - mirror ai_docs/mcp-server-git-repomix-output.xml structure to understand how to setup the mcp server 28 | - use ai_docs/paic-pkb-repomix-output.xml to get a rough understanding of what we're building. 29 | - libraries should be 30 | - click 31 | - mcp 32 | - pydantic 33 | - pytest (dev dependency) 34 | - sqlite3 (standard library) 35 | - use `uv add ` to add libraries. 36 | - we're using uv to manage the project. 37 | - add mcp-server-pocket-pick = "mcp_server_pocket_pick:main" to the project.scripts section in pyproject.toml 38 | 39 | ## API 40 | 41 | ``` 42 | pocket add \ 43 | --tags, t: str[] (optional) 44 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 45 | 46 | pocket find \ 47 | --mode: substr | fts | glob | regex | exact (optional) \ 48 | --limit, -l: number = 5 \ 49 | --info, -i: bool (show with metadata like id) \ 50 | --tags, -t: str[] (optional) \ 51 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 52 | 53 | pocket list \ 54 | --tags, -t: str[] (optional) \ 55 | --limit, -l: number = 100 \ 56 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 57 | 58 | pocket list-tags \ 59 | --limit, -l: number = 1000 \ 60 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 61 | 62 | pocket remove \ 63 | --id, -i: str \ 64 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 65 | 66 | pocket get \ 67 | --id, -i: str \ 68 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 69 | 70 | pocket backup \ 71 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 72 | ``` 73 | 74 | ### Example API Calls (for find modes) 75 | ``` 76 | # basic sqlite substring search 77 | pocket find "test" --mode substr 78 | 79 | # full text search 80 | pocket find "test" --mode fts 81 | 82 | # glob search 83 | pocket find "test*" --mode glob 84 | 85 | # regex search 86 | pocket find "^start.*test.*$" --mode regex 87 | 88 | # exact search 89 | pocket find "match exactly test" --mode exact 90 | ``` 91 | 92 | ## Project Structure 93 | - src/ 94 | - mcp_server_pocket_pick/ 95 | - __init__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml 96 | - __main__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml 97 | - server.py - MIRROR but use our functionality 98 | - serve(sqlite_database: Path | None) -> None 99 | - pass sqlite_database to every tool call (--db arg) 100 | - modules/ 101 | - __init__.py 102 | - init_db.py 103 | - data_types.py 104 | - class AddCommand(BaseModel) {text: str, tags: list[str] = [], db_path: Path = DEFAULT_SQLITE_DATABASE_PATH} 105 | - ... 106 | - constants.py 107 | - DEFAULT_SQLITE_DATABASE_PATH: Path = Path.home() / ".pocket_pick.db" 108 | - functionality/ 109 | - add.py 110 | - find.py 111 | - list.py 112 | - list_tags.py 113 | - remove.py 114 | - get.py 115 | - backup.py 116 | - tests/ 117 | - __init__.py 118 | - test_init_db.py 119 | - functionality/ 120 | - test_add.py 121 | - test_find.py 122 | - test_list.py 123 | - test_list_tags.py 124 | - test_remove.py 125 | - test_get.py 126 | - test_backup.py 127 | 128 | 129 | ## Validation (close the loop) 130 | - use `uv run pytest` to validate the tests pass. 131 | - use `uv run mcp-server-pocket-pick --help` to validate the mcp server works. -------------------------------------------------------------------------------- /specs/pocket_list_ids.md: -------------------------------------------------------------------------------- 1 | # Pocket Pick List IDs Feature 2 | 3 | ## Overview 4 | The `pocket_list_ids` command will provide a simple way to retrieve just the IDs of all items stored in the Pocket Pick database. This lightweight command is useful for quickly checking what IDs exist without having to retrieve the full content of each item. 5 | 6 | ## Command Details 7 | 8 | ### Name 9 | `pocket_list_ids` 10 | 11 | ### Description 12 | List all item IDs in your pocket pick database, optionally filtered by tags 13 | 14 | ### Parameters 15 | - `tags` (optional): List of strings - Filter results to only include items with these tags 16 | - `limit` (optional): Integer - Maximum number of IDs to return (default: 100) 17 | - `db` (optional): String - Path to the database file (defaults to "~/.pocket_pick.db") 18 | 19 | ### Response Format 20 | The command will return a simple list of strings, with each string representing an item ID. 21 | 22 | Example response: 23 | ``` 24 | python-sort-algorithm 25 | css-flexbox-cheatsheet 26 | docker-cheat-sheet 27 | api-design-patterns 28 | git-workflow 29 | ``` 30 | 31 | ## Implementation Plan 32 | 33 | ### 1. Data Model 34 | Create a `ListIdsCommand` model in `data_types.py`: 35 | ```python 36 | class ListIdsCommand(BaseModel): 37 | tags: List[str] = [] 38 | limit: int = 100 39 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 40 | ``` 41 | 42 | ### 2. Tool Definition 43 | Add a new tool to the `PocketTools` enum in `server.py`: 44 | ```python 45 | class PocketTools(str, Enum): 46 | ... 47 | LIST_IDS = "pocket_list_ids" 48 | ... 49 | ``` 50 | 51 | ### 3. Functionality 52 | Create a new file `list_ids.py` in the functionality directory: 53 | ```python 54 | from typing import List 55 | import sqlite3 56 | import json 57 | from ..data_types import ListIdsCommand 58 | 59 | def list_ids(command: ListIdsCommand) -> List[str]: 60 | """List all item IDs in the database, optionally filtered by tags.""" 61 | connection = sqlite3.connect(command.db_path) 62 | cursor = connection.cursor() 63 | 64 | if command.tags: 65 | # If tags are provided, filter by them 66 | placeholders = ', '.join(['?'] * len(command.tags)) 67 | query = f""" 68 | SELECT id 69 | FROM POCKET_PICK 70 | WHERE id IN ( 71 | SELECT id 72 | FROM POCKET_PICK 73 | WHERE ( 74 | SELECT COUNT(*) 75 | FROM json_each(tags) 76 | WHERE json_each.value IN ({placeholders}) 77 | ) = ? 78 | ) 79 | ORDER BY created DESC 80 | LIMIT ? 81 | """ 82 | params = [*command.tags, len(command.tags), command.limit] 83 | else: 84 | # If no tags provided, get all IDs 85 | query = """ 86 | SELECT id 87 | FROM POCKET_PICK 88 | ORDER BY created DESC 89 | LIMIT ? 90 | """ 91 | params = [command.limit] 92 | 93 | cursor.execute(query, params) 94 | results = [row[0] for row in cursor.fetchall()] 95 | 96 | cursor.close() 97 | connection.close() 98 | 99 | return results 100 | ``` 101 | 102 | ### 4. Server Integration 103 | Add the tool to the server's `list_tools()` method: 104 | ```python 105 | Tool( 106 | name=PocketTools.LIST_IDS, 107 | description="List all item IDs in your pocket pick database, optionally filtered by tags", 108 | inputSchema=PocketListIds.schema(), 109 | ), 110 | ``` 111 | 112 | ### 5. Handler Implementation 113 | Add a case to the `call_tool()` method: 114 | ```python 115 | case PocketTools.LIST_IDS: 116 | command = ListIdsCommand( 117 | tags=arguments.get("tags", []), 118 | limit=arguments.get("limit", 100), 119 | db_path=db_path 120 | ) 121 | results = list_ids(command) 122 | 123 | if not results: 124 | return [TextContent( 125 | type="text", 126 | text="No item IDs found." 127 | )] 128 | 129 | return [TextContent( 130 | type="text", 131 | text="\n".join(results) 132 | )] 133 | ``` 134 | 135 | ## Benefits 136 | 1. Provides a lightweight way to see all available IDs 137 | 2. Useful for checking if an ID exists before adding a new item 138 | 3. Can be used to generate reports or lists of available knowledge 139 | 4. Supports the same tag filtering as other list commands for consistency 140 | 141 | ## Testing 142 | Testing should verify that: 143 | 1. All IDs are correctly returned when no filters are applied 144 | 2. Tag filtering correctly limits the IDs returned 145 | 3. The limit parameter correctly restricts the number of results 146 | 4. Results are ordered by creation date (newest first) 147 | 5. The response format is a simple list of strings -------------------------------------------------------------------------------- /specs/require-id-during-add-feature.md: -------------------------------------------------------------------------------- 1 | # Feature Specification: Require ID for Adding Items 2 | 3 | ## High Level Objective 4 | Currently, when users add items to the Pocket Pick database using the `pocket_add` and `pocket_add_file` tools, IDs are automatically generated. This feature will modify these tools to require users to provide an ID as a mandatory parameter when adding items, giving them more control over item identification and enabling custom ID schemes. 5 | 6 | Benefits: 7 | - Allows users to create meaningful, custom IDs instead of auto-generated UUIDs 8 | - Enables semantic naming for easier item recall 9 | - Provides consistency with other commands that require IDs 10 | - Maintains backward compatibility with existing database records 11 | 12 | ## Type Changes 13 | 14 | ### Data Types Module 15 | **File**: `src/mcp_server_pocket_pick/modules/data_types.py` 16 | 17 | 1. Update `AddCommand` class: 18 | ```python 19 | class AddCommand(BaseModel): 20 | id: str # New required field 21 | text: str 22 | tags: List[str] = [] 23 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 24 | ``` 25 | 26 | 2. Update `AddFileCommand` class: 27 | ```python 28 | class AddFileCommand(BaseModel): 29 | id: str # New required field 30 | file_path: str 31 | tags: List[str] = [] 32 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 33 | ``` 34 | 35 | ### Server Module 36 | **File**: `src/mcp_server_pocket_pick/server.py` 37 | 38 | 1. Update `PocketAdd` class: 39 | ```python 40 | class PocketAdd(BaseModel): 41 | id: str # New required field 42 | text: str 43 | tags: List[str] = [] 44 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 45 | ``` 46 | 47 | 2. Update `PocketAddFile` class: 48 | ```python 49 | class PocketAddFile(BaseModel): 50 | id: str # New required field 51 | file_path: str 52 | tags: List[str] = [] 53 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 54 | ``` 55 | 56 | ## Method Changes 57 | 58 | ### Add Functionality Module 59 | **File**: `src/mcp_server_pocket_pick/modules/functionality/add.py` 60 | 61 | 1. Modify the `add` function: 62 | - Remove UUID generation code 63 | - Use the provided ID from `command.id` 64 | - Add error handling for duplicate IDs 65 | - Return a clear error message if the ID already exists 66 | 67 | ### Add File Functionality Module 68 | **File**: `src/mcp_server_pocket_pick/modules/functionality/add_file.py` 69 | 70 | 1. Similar changes to the `add_file` function: 71 | - Remove UUID generation code 72 | - Use the provided ID from `command.id` 73 | - Add error handling for duplicate IDs 74 | - Return a clear error message if the ID already exists 75 | 76 | ### Server Implementation 77 | **File**: `src/mcp_server_pocket_pick/server.py` 78 | 79 | 1. Update the `call_tool` function to pass the ID from arguments: 80 | ```python 81 | # In PocketTools.ADD case: 82 | command = AddCommand( 83 | id=arguments["id"], # Pass the ID from arguments 84 | text=arguments["text"], 85 | tags=arguments.get("tags", []), 86 | db_path=db_path 87 | ) 88 | 89 | # In PocketTools.ADD_FILE case: 90 | command = AddFileCommand( 91 | id=arguments["id"], # Pass the ID from arguments 92 | file_path=arguments["file_path"], 93 | tags=arguments.get("tags", []), 94 | db_path=db_path 95 | ) 96 | ``` 97 | 98 | 2. Add error handling for duplicate IDs in both cases 99 | 100 | ## Test Changes 101 | 102 | ### Add Tests 103 | **File**: `src/mcp_server_pocket_pick/tests/functionality/test_add.py` 104 | 105 | 1. Update existing tests to include an ID parameter 106 | 2. Add new test cases: 107 | - Test successful addition with a custom ID 108 | - Test error case when adding an item with a duplicate ID 109 | - Test with different ID formats and edge cases 110 | 111 | ### Add File Tests 112 | **File**: `src/mcp_server_pocket_pick/tests/functionality/test_add_file.py` 113 | 114 | 1. Update existing tests to include an ID parameter 115 | 2. Add new test cases: 116 | - Test successful addition with a custom ID 117 | - Test error case when adding an item with a duplicate ID 118 | - Test with different ID formats and edge cases 119 | 120 | Check the other tests to see if they're using any add functions and update them to use the new ID parameter. 121 | file: `src/mcp_server_pocket_pick/tests/functionality/*` 122 | 123 | ## Self Validation 124 | 125 | 1. **Manual Testing**: 126 | - Add an item with a custom ID 127 | - Verify it was added correctly 128 | - Try to add another item with the same ID and verify the error 129 | - Get, list, and remove the item using the custom ID 130 | 131 | 2. **Automated Testing**: 132 | - Run all tests with `uv run pytest -v` 133 | - Verify all tests pass 134 | 135 | ## README Update 136 | **File**: `README.md` 137 | 138 | 1. Update tool descriptions in the "Pocket Pick MCP Tools" section: 139 | - `pocket_add`: "Add a new item with a specified ID to your knowledge base" 140 | - `pocket_add_file`: "Add a file's content with a specified ID to your knowledge base" 141 | 142 | 2. Update examples in the "Using with Claude" section to include IDs: 143 | ``` 144 | Add "claude mcp list" as a pocket pick item with ID "claude-mcp-list". tags: mcp, claude, code 145 | ``` 146 | 147 | 3. Add a new section on ID management: 148 | ```markdown 149 | ## ID Management 150 | 151 | When adding items to Pocket Pick, you must now provide a unique ID: 152 | 153 | - IDs must be unique across your database 154 | - Choose descriptive IDs that help you identify the content 155 | - If you attempt to add an item with an ID that already exists, you'll receive an error 156 | -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | from pathlib import Path 3 | import logging 4 | import sys 5 | from .server import serve 6 | 7 | @click.command() 8 | @click.option("--database", "-d", type=Path, help="SQLite database path (default: ~/.pocket_pick.db)") 9 | @click.option("-v", "--verbose", count=True) 10 | def main(database: Path | None, verbose: bool) -> None: 11 | """Pocket Pick - Your Personal Knowledge Base""" 12 | import asyncio 13 | 14 | logging_level = logging.WARN 15 | if verbose == 1: 16 | logging_level = logging.INFO 17 | elif verbose >= 2: 18 | logging_level = logging.DEBUG 19 | 20 | logging.basicConfig(level=logging_level, stream=sys.stderr) 21 | asyncio.run(serve(database)) 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/__main__.py: -------------------------------------------------------------------------------- 1 | from mcp_server_pocket_pick import main 2 | 3 | main() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # Module initialization -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/data_types.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from pydantic import BaseModel 3 | from typing import List, Optional 4 | from datetime import datetime 5 | from .constants import DEFAULT_SQLITE_DATABASE_PATH 6 | 7 | 8 | class AddCommand(BaseModel): 9 | id: str 10 | text: str 11 | tags: List[str] = [] 12 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 13 | 14 | 15 | class AddFileCommand(BaseModel): 16 | id: str 17 | file_path: str 18 | tags: List[str] = [] 19 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 20 | 21 | 22 | class FindCommand(BaseModel): 23 | text: str 24 | mode: str = "substr" # substr | fts | glob | regex | exact 25 | limit: int = 5 26 | info: bool = False 27 | tags: List[str] = [] 28 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 29 | 30 | 31 | class ListCommand(BaseModel): 32 | tags: List[str] = [] 33 | limit: int = 100 34 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 35 | 36 | 37 | class ListTagsCommand(BaseModel): 38 | limit: int = 1000 39 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 40 | 41 | 42 | class RemoveCommand(BaseModel): 43 | id: str 44 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 45 | 46 | 47 | class GetCommand(BaseModel): 48 | id: str 49 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 50 | 51 | 52 | class BackupCommand(BaseModel): 53 | backup_path: Path 54 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 55 | 56 | 57 | class ToFileByIdCommand(BaseModel): 58 | id: str 59 | output_file_path_abs: Path 60 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 61 | 62 | 63 | class PocketItem(BaseModel): 64 | id: str 65 | created: datetime 66 | text: str 67 | tags: List[str] 68 | 69 | 70 | class ListIdsCommand(BaseModel): 71 | tags: List[str] = [] 72 | limit: int = 100 73 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 74 | -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/__init__.py: -------------------------------------------------------------------------------- 1 | # Functionality module initialization -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/add.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import uuid 3 | import json 4 | from datetime import datetime 5 | from pathlib import Path 6 | import logging 7 | from ..data_types import AddCommand, PocketItem 8 | from ..init_db import init_db, normalize_tags 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | def add(command: AddCommand) -> PocketItem: 13 | """ 14 | Add a new item to the pocket pick database 15 | 16 | Args: 17 | command: AddCommand with id, text, tags and db_path 18 | 19 | Returns: 20 | PocketItem: The newly created item 21 | 22 | Raises: 23 | sqlite3.IntegrityError: If an item with the same ID already exists 24 | """ 25 | # Normalize tags 26 | normalized_tags = normalize_tags(command.tags) 27 | 28 | # Use the provided ID 29 | item_id = command.id 30 | 31 | # Get current timestamp 32 | timestamp = datetime.now() 33 | 34 | # Connect to database 35 | db = init_db(command.db_path) 36 | 37 | try: 38 | # Serialize tags to JSON 39 | tags_json = json.dumps(normalized_tags) 40 | 41 | # Insert item 42 | try: 43 | db.execute( 44 | "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", 45 | (item_id, timestamp.isoformat(), command.text, tags_json) 46 | ) 47 | 48 | # Commit transaction 49 | db.commit() 50 | 51 | # Return created item 52 | return PocketItem( 53 | id=item_id, 54 | created=timestamp, 55 | text=command.text, 56 | tags=normalized_tags 57 | ) 58 | except sqlite3.IntegrityError: 59 | logger.error(f"Item with ID {item_id} already exists") 60 | raise sqlite3.IntegrityError(f"Item with ID {item_id} already exists") 61 | except Exception as e: 62 | logger.error(f"Error adding item: {e}") 63 | raise 64 | finally: 65 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/add_file.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import uuid 3 | import json 4 | from datetime import datetime 5 | from pathlib import Path 6 | import logging 7 | from ..data_types import AddFileCommand, PocketItem 8 | from ..init_db import init_db, normalize_tags 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | def add_file(command: AddFileCommand) -> PocketItem: 13 | """ 14 | Add a new item to the pocket pick database from a file 15 | 16 | Args: 17 | command: AddFileCommand with id, file_path, tags and db_path 18 | 19 | Returns: 20 | PocketItem: The newly created item 21 | 22 | Raises: 23 | sqlite3.IntegrityError: If an item with the same ID already exists 24 | FileNotFoundError: If the specified file doesn't exist 25 | """ 26 | # Read the file content 27 | try: 28 | file_path = Path(command.file_path) 29 | if not file_path.exists(): 30 | raise FileNotFoundError(f"File not found: {file_path}") 31 | 32 | with open(file_path, 'r', encoding='utf-8') as f: 33 | text = f.read() 34 | except Exception as e: 35 | logger.error(f"Error reading file {command.file_path}: {e}") 36 | raise 37 | 38 | # Normalize tags 39 | normalized_tags = normalize_tags(command.tags) 40 | 41 | # Use the provided ID 42 | item_id = command.id 43 | 44 | # Get current timestamp 45 | timestamp = datetime.now() 46 | 47 | # Connect to database 48 | db = init_db(command.db_path) 49 | 50 | try: 51 | # Serialize tags to JSON 52 | tags_json = json.dumps(normalized_tags) 53 | 54 | # Insert item 55 | try: 56 | db.execute( 57 | "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", 58 | (item_id, timestamp.isoformat(), text, tags_json) 59 | ) 60 | 61 | # Commit transaction 62 | db.commit() 63 | 64 | # Return created item 65 | return PocketItem( 66 | id=item_id, 67 | created=timestamp, 68 | text=text, 69 | tags=normalized_tags 70 | ) 71 | except sqlite3.IntegrityError: 72 | logger.error(f"Item with ID {item_id} already exists") 73 | raise sqlite3.IntegrityError(f"Item with ID {item_id} already exists") 74 | except Exception as e: 75 | logger.error(f"Error adding item from file: {e}") 76 | raise 77 | finally: 78 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/backup.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import shutil 3 | import logging 4 | from ..data_types import BackupCommand 5 | from ..init_db import init_db 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | def backup(command: BackupCommand) -> bool: 10 | """ 11 | Backup the pocket pick database to a specified location 12 | 13 | Args: 14 | command: BackupCommand with backup destination path 15 | 16 | Returns: 17 | bool: True if backup was successful, False otherwise 18 | """ 19 | # Make sure source DB exists by initializing it if needed 20 | db = init_db(command.db_path) 21 | db.close() 22 | 23 | try: 24 | # Create parent directories if they don't exist 25 | command.backup_path.parent.mkdir(parents=True, exist_ok=True) 26 | 27 | # Copy the database file to the backup location 28 | shutil.copy2(command.db_path, command.backup_path) 29 | 30 | # Verify the backup file exists 31 | if command.backup_path.exists(): 32 | logger.info(f"Backup created successfully at {command.backup_path}") 33 | return True 34 | else: 35 | logger.error(f"Backup file not found at {command.backup_path}") 36 | return False 37 | except Exception as e: 38 | logger.error(f"Error creating backup: {e}") 39 | return False -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/find.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | from datetime import datetime 4 | from typing import List 5 | import logging 6 | import re 7 | from ..data_types import FindCommand, PocketItem 8 | from ..init_db import init_db, normalize_tags 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | def find(command: FindCommand) -> List[PocketItem]: 13 | """ 14 | Find items in the pocket pick database matching the search criteria 15 | 16 | Args: 17 | command: FindCommand with search parameters 18 | 19 | Returns: 20 | List[PocketItem]: List of matching items 21 | """ 22 | # Normalize tags 23 | normalized_tags = normalize_tags(command.tags) if command.tags else [] 24 | 25 | # Connect to database 26 | db = init_db(command.db_path) 27 | 28 | try: 29 | # Base query 30 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 31 | params = [] 32 | where_clauses = [] 33 | 34 | # Apply search mode 35 | if command.text: 36 | if command.mode == "substr": 37 | where_clauses.append("text LIKE ?") 38 | params.append(f"%{command.text}%") 39 | elif command.mode == "fts": 40 | try: 41 | # First, try using FTS5 virtual table 42 | # Replace normal query with FTS query 43 | query = """ 44 | SELECT POCKET_PICK.id, POCKET_PICK.created, POCKET_PICK.text, POCKET_PICK.tags 45 | FROM pocket_pick_fts 46 | JOIN POCKET_PICK ON pocket_pick_fts.rowid = POCKET_PICK.rowid 47 | """ 48 | 49 | # FTS5 query syntax 50 | if command.mode == "fts": 51 | # Check for different query formats 52 | 53 | # Direct quoted phrase - user already provided quotes for exact phrases 54 | if command.text.startswith('"') and command.text.endswith('"'): 55 | # User wants exact phrase matching (e.g., "word1 word2") 56 | # Just use it directly - FTS5 understands quoted phrases 57 | search_term = command.text 58 | logger.debug(f"Using quoted phrase search: {search_term}") 59 | 60 | # Multi-word regular search 61 | elif ' ' in command.text: 62 | # Default: Match all terms independently (AND behavior) 63 | search_term = command.text 64 | 65 | # Single word search 66 | else: 67 | search_term = command.text 68 | else: 69 | search_term = command.text 70 | 71 | # Using standard FTS5 query approach 72 | 73 | # Set up FTS5 query parameters 74 | where_clauses = [f"pocket_pick_fts MATCH ?"] 75 | params = [search_term] 76 | 77 | # FTS5 table doesn't have these columns, so we need to add tags filter separately 78 | if normalized_tags: 79 | tag_clauses = [] 80 | for tag in normalized_tags: 81 | tag_clauses.append("POCKET_PICK.tags LIKE ?") 82 | params.append(f"%\"{tag}\"%") 83 | 84 | where_clauses.append(f"({' AND '.join(tag_clauses)})") 85 | 86 | # We'll handle the query execution in a special way 87 | use_fts5 = True 88 | except sqlite3.OperationalError: 89 | # Fallback to basic LIKE-based search if FTS5 is not available 90 | logger.warning("FTS5 not available, falling back to basic search") 91 | use_fts5 = False 92 | 93 | # Standard fallback approach (original implementation) 94 | search_words = command.text.split() 95 | word_clauses = [] 96 | for word in search_words: 97 | word_clauses.append("text LIKE ?") 98 | params.append(f"%{word}%") 99 | where_clauses.append(f"({' AND '.join(word_clauses)})") 100 | elif command.mode == "glob": 101 | where_clauses.append("text GLOB ?") 102 | params.append(command.text) 103 | elif command.mode == "regex": 104 | # We'll need to filter with regex after query 105 | pass 106 | elif command.mode == "exact": 107 | where_clauses.append("text = ?") 108 | params.append(command.text) 109 | 110 | # Apply tag filter if tags are specified 111 | if normalized_tags: 112 | # Find items that have all the specified tags 113 | # We need to check if each tag exists in the JSON array 114 | tag_clauses = [] 115 | for tag in normalized_tags: 116 | tag_clauses.append("tags LIKE ?") 117 | # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets 118 | params.append(f"%\"{tag}\"%") 119 | 120 | where_clauses.append(f"({' AND '.join(tag_clauses)})") 121 | 122 | # Handle query construction based on whether we're using FTS5 123 | if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: 124 | # For FTS5, we've already constructed the base query 125 | if where_clauses: 126 | query += f" WHERE {' AND '.join(where_clauses)}" 127 | 128 | # Special ordering for FTS5 to get the best matches first 129 | query += f" ORDER BY rank, created DESC LIMIT {command.limit}" 130 | 131 | logger.debug(f"Using FTS5 query: {query}") 132 | else: 133 | # Standard query construction 134 | if where_clauses: 135 | query += f" WHERE {' AND '.join(where_clauses)}" 136 | 137 | # Apply limit 138 | query += f" ORDER BY created DESC LIMIT {command.limit}" 139 | 140 | # Execute query 141 | try: 142 | cursor = db.execute(query, params) 143 | except sqlite3.OperationalError as e: 144 | # If the FTS5 query fails, fall back to the basic query 145 | if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: 146 | logger.warning(f"FTS5 query failed: {e}. Falling back to basic search.") 147 | 148 | # Reset to base query 149 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 150 | params = [] 151 | 152 | # Standard fallback approach 153 | if command.text: 154 | search_words = command.text.split() 155 | word_clauses = [] 156 | for word in search_words: 157 | word_clauses.append("text LIKE ?") 158 | params.append(f"%{word}%") 159 | query += f" WHERE ({' AND '.join(word_clauses)})" 160 | 161 | # Re-add tag filters if needed 162 | if normalized_tags: 163 | tag_clauses = [] 164 | for tag in normalized_tags: 165 | tag_clauses.append("tags LIKE ?") 166 | params.append(f"%\"{tag}\"%") 167 | 168 | query += f" AND ({' AND '.join(tag_clauses)})" 169 | 170 | query += f" ORDER BY created DESC LIMIT {command.limit}" 171 | cursor = db.execute(query, params) 172 | else: 173 | # If it's not an FTS5 issue, re-raise the exception 174 | raise 175 | 176 | # Process results 177 | results = [] 178 | for row in cursor.fetchall(): 179 | id, created_str, text, tags_json = row 180 | 181 | # Parse the created timestamp 182 | created = datetime.fromisoformat(created_str) 183 | 184 | # Parse the tags JSON 185 | tags = json.loads(tags_json) 186 | 187 | # Create item 188 | item = PocketItem( 189 | id=id, 190 | created=created, 191 | text=text, 192 | tags=tags 193 | ) 194 | 195 | # Apply regex filter if needed (we do this after the SQL query) 196 | if command.mode == "regex" and command.text: 197 | try: 198 | pattern = re.compile(command.text, re.IGNORECASE) 199 | if not pattern.search(text): 200 | continue 201 | except re.error: 202 | logger.warning(f"Invalid regex pattern: {command.text}") 203 | continue 204 | 205 | results.append(item) 206 | 207 | return results 208 | except Exception as e: 209 | logger.error(f"Error finding items: {e}") 210 | raise 211 | finally: 212 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/get.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | from datetime import datetime 4 | import logging 5 | from typing import Optional 6 | from ..data_types import GetCommand, PocketItem 7 | from ..init_db import init_db 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | def get(command: GetCommand) -> Optional[PocketItem]: 12 | """ 13 | Get an item from the pocket pick database by ID 14 | 15 | Args: 16 | command: GetCommand with item ID 17 | 18 | Returns: 19 | Optional[PocketItem]: The item if found, None otherwise 20 | """ 21 | # Connect to database 22 | db = init_db(command.db_path) 23 | 24 | try: 25 | # Query for item with given ID 26 | cursor = db.execute( 27 | "SELECT id, created, text, tags FROM POCKET_PICK WHERE id = ?", 28 | (command.id,) 29 | ) 30 | 31 | # Fetch the row 32 | row = cursor.fetchone() 33 | 34 | # If no row was found, return None 35 | if row is None: 36 | return None 37 | 38 | # Process the row 39 | id, created_str, text, tags_json = row 40 | 41 | # Parse the created timestamp 42 | created = datetime.fromisoformat(created_str) 43 | 44 | # Parse the tags JSON 45 | tags = json.loads(tags_json) 46 | 47 | # Create and return the item 48 | return PocketItem( 49 | id=id, 50 | created=created, 51 | text=text, 52 | tags=tags 53 | ) 54 | except Exception as e: 55 | logger.error(f"Error getting item {command.id}: {e}") 56 | raise 57 | finally: 58 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/list.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | from datetime import datetime 4 | from typing import List 5 | import logging 6 | from ..data_types import ListCommand, PocketItem 7 | from ..init_db import init_db, normalize_tags 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | def list_items(command: ListCommand) -> List[PocketItem]: 12 | """ 13 | List items in the pocket pick database, optionally filtered by tags 14 | 15 | Args: 16 | command: ListCommand with optional tag filters and limit 17 | 18 | Returns: 19 | List[PocketItem]: List of matching items 20 | """ 21 | # Normalize tags 22 | normalized_tags = normalize_tags(command.tags) if command.tags else [] 23 | 24 | # Connect to database 25 | db = init_db(command.db_path) 26 | 27 | try: 28 | # Base query 29 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 30 | params = [] 31 | 32 | # Apply tag filter if tags are specified 33 | if normalized_tags: 34 | # We need to check if each tag exists in the JSON array 35 | tag_clauses = [] 36 | for tag in normalized_tags: 37 | tag_clauses.append("tags LIKE ?") 38 | # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets 39 | params.append(f"%\"{tag}\"%") 40 | 41 | query += f" WHERE {' AND '.join(tag_clauses)}" 42 | 43 | # Apply order and limit 44 | query += f" ORDER BY created DESC LIMIT {command.limit}" 45 | 46 | # Execute query 47 | cursor = db.execute(query, params) 48 | 49 | # Process results 50 | results = [] 51 | for row in cursor.fetchall(): 52 | id, created_str, text, tags_json = row 53 | 54 | # Parse the created timestamp 55 | created = datetime.fromisoformat(created_str) 56 | 57 | # Parse the tags JSON 58 | tags = json.loads(tags_json) 59 | 60 | # Create item 61 | item = PocketItem( 62 | id=id, 63 | created=created, 64 | text=text, 65 | tags=tags 66 | ) 67 | 68 | results.append(item) 69 | 70 | return results 71 | except Exception as e: 72 | logger.error(f"Error listing items: {e}") 73 | raise 74 | finally: 75 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/list_ids.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import List 3 | import logging 4 | from ..data_types import ListIdsCommand 5 | from ..init_db import init_db, normalize_tags 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | def list_ids(command: ListIdsCommand) -> List[str]: 10 | """List all item IDs in the database, optionally filtered by tags.""" 11 | normalized_tags = normalize_tags(command.tags) if command.tags else [] 12 | 13 | db = init_db(command.db_path) 14 | 15 | try: 16 | if normalized_tags: 17 | placeholders = ', '.join(['?'] * len(normalized_tags)) 18 | query = f""" 19 | SELECT id 20 | FROM POCKET_PICK 21 | WHERE id IN ( 22 | SELECT id 23 | FROM POCKET_PICK 24 | WHERE ( 25 | SELECT COUNT(*) 26 | FROM json_each(tags) 27 | WHERE json_each.value IN ({placeholders}) 28 | ) = ? 29 | ) 30 | ORDER BY created DESC 31 | LIMIT ? 32 | """ 33 | params = [*normalized_tags, len(normalized_tags), command.limit] 34 | else: 35 | query = """ 36 | SELECT id 37 | FROM POCKET_PICK 38 | ORDER BY created DESC 39 | LIMIT ? 40 | """ 41 | params = [command.limit] 42 | 43 | cursor = db.execute(query, params) 44 | results = [row[0] for row in cursor.fetchall()] 45 | return results 46 | except Exception as e: 47 | logger.error(f"Error listing ids: {e}") 48 | raise 49 | finally: 50 | db.close() 51 | -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/list_tags.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | from typing import List, Dict 4 | import logging 5 | from ..data_types import ListTagsCommand 6 | from ..init_db import init_db 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | def list_tags(command: ListTagsCommand) -> List[Dict[str, int]]: 11 | """ 12 | List all tags in the pocket pick database with their counts 13 | 14 | Args: 15 | command: ListTagsCommand with limit 16 | 17 | Returns: 18 | List[Dict[str, int]]: List of dicts with tag name and count 19 | """ 20 | # Connect to database 21 | db = init_db(command.db_path) 22 | 23 | try: 24 | # Get all tags with their counts 25 | cursor = db.execute("SELECT tags FROM POCKET_PICK") 26 | 27 | # Process results to count tags 28 | tag_counts = {} 29 | for (tags_json,) in cursor.fetchall(): 30 | tags = json.loads(tags_json) 31 | for tag in tags: 32 | if tag in tag_counts: 33 | tag_counts[tag] += 1 34 | else: 35 | tag_counts[tag] = 1 36 | 37 | # Sort by count (descending) and then alphabetically 38 | sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0])) 39 | 40 | # Apply limit and format result 41 | result = [{"tag": tag, "count": count} for tag, count in sorted_tags[:command.limit]] 42 | 43 | return result 44 | except Exception as e: 45 | logger.error(f"Error listing tags: {e}") 46 | raise 47 | finally: 48 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/remove.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import logging 3 | from ..data_types import RemoveCommand 4 | from ..init_db import init_db 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | def remove(command: RemoveCommand) -> bool: 9 | """ 10 | Remove an item from the pocket pick database by ID 11 | 12 | Args: 13 | command: RemoveCommand with item ID 14 | 15 | Returns: 16 | bool: True if an item was removed, False if no matching item was found 17 | """ 18 | # Connect to database 19 | db = init_db(command.db_path) 20 | 21 | try: 22 | # Delete item with given ID 23 | cursor = db.execute("DELETE FROM POCKET_PICK WHERE id = ?", (command.id,)) 24 | 25 | # Commit the transaction 26 | db.commit() 27 | 28 | # Check if any row was affected 29 | return cursor.rowcount > 0 30 | except Exception as e: 31 | logger.error(f"Error removing item {command.id}: {e}") 32 | raise 33 | finally: 34 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/functionality/to_file_by_id.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import logging 3 | import os 4 | from ..data_types import ToFileByIdCommand, PocketItem 5 | from .get import get 6 | from .get import GetCommand 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | def to_file_by_id(command: ToFileByIdCommand) -> bool: 11 | """ 12 | Write pocket pick content with given ID to the specified file 13 | 14 | Args: 15 | command: ToFileByIdCommand with id, output_file_path and db_path 16 | 17 | Returns: 18 | bool: True if successful, False otherwise 19 | """ 20 | try: 21 | # First get the item from the database 22 | get_command = GetCommand( 23 | id=command.id, 24 | db_path=command.db_path 25 | ) 26 | 27 | item = get(get_command) 28 | 29 | if not item: 30 | logger.error(f"Item with ID {command.id} not found") 31 | return False 32 | 33 | # Ensure parent directory exists 34 | output_path = Path(command.output_file_path_abs) 35 | output_path.parent.mkdir(parents=True, exist_ok=True) 36 | 37 | # Write content to file 38 | with open(output_path, 'w', encoding='utf-8') as f: 39 | f.write(item.text) 40 | 41 | return True 42 | except Exception as e: 43 | logger.error(f"Error writing to file {command.output_file_path_abs}: {e}") 44 | return False -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/modules/init_db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from pathlib import Path 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | def init_db(db_path: Path) -> sqlite3.Connection: 8 | """Initialize SQLite database with POCKET_PICK table""" 9 | # Ensure parent directory exists 10 | db_path.parent.mkdir(parents=True, exist_ok=True) 11 | 12 | logger.info(f"Initializing database at {db_path}") 13 | # Ensure the directory exists 14 | if not db_path.parent.exists(): 15 | logger.info(f"Creating directory {db_path.parent}") 16 | db_path.parent.mkdir(parents=True, exist_ok=True) 17 | 18 | db = sqlite3.connect(str(db_path)) 19 | 20 | # Enable foreign keys 21 | db.execute("PRAGMA foreign_keys = ON") 22 | 23 | # Create the POCKET_PICK table 24 | db.execute(""" 25 | CREATE TABLE IF NOT EXISTS POCKET_PICK ( 26 | id TEXT PRIMARY KEY, 27 | created TIMESTAMP NOT NULL, 28 | text TEXT NOT NULL, 29 | tags TEXT NOT NULL 30 | ) 31 | """) 32 | 33 | # Create indexes for efficient searching 34 | db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_created ON POCKET_PICK(created)") 35 | db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_text ON POCKET_PICK(text)") 36 | 37 | # Create FTS5 virtual table for full-text search 38 | try: 39 | db.execute(""" 40 | CREATE VIRTUAL TABLE IF NOT EXISTS pocket_pick_fts USING fts5( 41 | text, 42 | content='POCKET_PICK', 43 | content_rowid='rowid' 44 | ) 45 | """) 46 | 47 | # Create triggers to keep FTS index up to date 48 | db.execute(""" 49 | CREATE TRIGGER IF NOT EXISTS pocket_pick_ai AFTER INSERT ON POCKET_PICK 50 | BEGIN 51 | INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); 52 | END 53 | """) 54 | 55 | db.execute(""" 56 | CREATE TRIGGER IF NOT EXISTS pocket_pick_ad AFTER DELETE ON POCKET_PICK 57 | BEGIN 58 | INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); 59 | END 60 | """) 61 | 62 | db.execute(""" 63 | CREATE TRIGGER IF NOT EXISTS pocket_pick_au AFTER UPDATE ON POCKET_PICK 64 | BEGIN 65 | INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); 66 | INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); 67 | END 68 | """) 69 | 70 | # Rebuild FTS index if needed (for existing data) 71 | db.execute(""" 72 | INSERT OR IGNORE INTO pocket_pick_fts(rowid, text) 73 | SELECT rowid, text FROM POCKET_PICK 74 | """) 75 | 76 | except sqlite3.OperationalError as e: 77 | # If FTS5 is not available, log a warning but continue 78 | logger.warning(f"FTS5 extension not available: {e}. Full-text search will fallback to basic search.") 79 | 80 | # Commit changes 81 | db.commit() 82 | 83 | return db 84 | 85 | def normalize_tag(tag: str) -> str: 86 | """ 87 | Normalize tags: 88 | - lowercase 89 | - trim whitespace 90 | - replace spaces and underscores with dashes 91 | """ 92 | tag = tag.lower().strip() 93 | return tag.replace(' ', '-').replace('_', '-') 94 | 95 | def normalize_tags(tags: list[str]) -> list[str]: 96 | """Apply normalization to a list of tags""" 97 | return [normalize_tag(tag) for tag in tags] -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disler/pocket-pick/d8243d35036cb4b3c934180fce30952bc4a73365/src/mcp_server_pocket_pick/py.typed -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Sequence, List 4 | from mcp.server import Server 5 | from mcp.server.session import ServerSession 6 | from mcp.server.stdio import stdio_server 7 | from mcp.types import ( 8 | ClientCapabilities, 9 | TextContent, 10 | Tool, 11 | ListRootsResult, 12 | RootsCapability, 13 | ) 14 | from enum import Enum 15 | from pydantic import BaseModel 16 | 17 | from .modules.data_types import ( 18 | AddCommand, 19 | AddFileCommand, 20 | FindCommand, 21 | ListCommand, 22 | ListTagsCommand, 23 | ListIdsCommand, 24 | RemoveCommand, 25 | GetCommand, 26 | BackupCommand, 27 | ToFileByIdCommand, 28 | ) 29 | from .modules.functionality.add import add 30 | from .modules.functionality.add_file import add_file 31 | from .modules.functionality.find import find 32 | from .modules.functionality.list import list_items 33 | from .modules.functionality.list_tags import list_tags 34 | from .modules.functionality.list_ids import list_ids 35 | from .modules.functionality.remove import remove 36 | from .modules.functionality.get import get 37 | from .modules.functionality.backup import backup 38 | from .modules.functionality.to_file_by_id import to_file_by_id 39 | from .modules.constants import DEFAULT_SQLITE_DATABASE_PATH 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | class PocketAdd(BaseModel): 44 | id: str 45 | text: str 46 | tags: List[str] = [] 47 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 48 | 49 | class PocketAddFile(BaseModel): 50 | id: str 51 | file_path: str 52 | tags: List[str] = [] 53 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 54 | 55 | class PocketFind(BaseModel): 56 | text: str 57 | mode: str = "substr" 58 | limit: int = 5 59 | info: bool = False 60 | tags: List[str] = [] 61 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 62 | 63 | class PocketList(BaseModel): 64 | tags: List[str] = [] 65 | limit: int = 100 66 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 67 | 68 | class PocketListTags(BaseModel): 69 | limit: int = 1000 70 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 71 | 72 | class PocketListIds(BaseModel): 73 | tags: List[str] = [] 74 | limit: int = 100 75 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 76 | 77 | class PocketRemove(BaseModel): 78 | id: str 79 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 80 | 81 | class PocketGet(BaseModel): 82 | id: str 83 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 84 | 85 | class PocketBackup(BaseModel): 86 | backup_path: str 87 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 88 | 89 | class PocketToFileById(BaseModel): 90 | id: str 91 | output_file_path_abs: str 92 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 93 | 94 | class PocketTools(str, Enum): 95 | ADD = "pocket_add" 96 | ADD_FILE = "pocket_add_file" 97 | FIND = "pocket_find" 98 | LIST = "pocket_list" 99 | LIST_TAGS = "pocket_list_tags" 100 | LIST_IDS = "pocket_list_ids" 101 | REMOVE = "pocket_remove" 102 | GET = "pocket_get" 103 | BACKUP = "pocket_backup" 104 | TO_FILE_BY_ID = "pocket_to_file_by_id" 105 | 106 | async def serve(sqlite_database: Path | None = None) -> None: 107 | logger.info(f"Starting Pocket Pick MCP server") 108 | 109 | # Determine which database path to use 110 | db_path = sqlite_database if sqlite_database is not None else DEFAULT_SQLITE_DATABASE_PATH 111 | logger.info(f"Using database at {db_path}") 112 | 113 | # Initialize the database at startup to ensure it exists 114 | from .modules.init_db import init_db 115 | connection = init_db(db_path) 116 | connection.close() 117 | logger.info(f"Database initialized at {db_path}") 118 | 119 | server = Server("pocket-pick") 120 | 121 | @server.list_tools() 122 | async def list_tools() -> list[Tool]: 123 | return [ 124 | Tool( 125 | name=PocketTools.ADD, 126 | description="Add a new item to your pocket pick database", 127 | inputSchema=PocketAdd.schema(), 128 | ), 129 | Tool( 130 | name=PocketTools.ADD_FILE, 131 | description="Add a new item to your pocket pick database from a file", 132 | inputSchema=PocketAddFile.schema(), 133 | ), 134 | Tool( 135 | name=PocketTools.FIND, 136 | description="Find items in your pocket pick database by text and tags", 137 | inputSchema=PocketFind.schema(), 138 | ), 139 | Tool( 140 | name=PocketTools.LIST, 141 | description="List items in your pocket pick database, optionally filtered by tags", 142 | inputSchema=PocketList.schema(), 143 | ), 144 | Tool( 145 | name=PocketTools.LIST_TAGS, 146 | description="List all tags in your pocket pick database with their counts", 147 | inputSchema=PocketListTags.schema(), 148 | ), 149 | Tool( 150 | name=PocketTools.LIST_IDS, 151 | description="List all item IDs in your pocket pick database, optionally filtered by tags", 152 | inputSchema=PocketListIds.schema(), 153 | ), 154 | Tool( 155 | name=PocketTools.REMOVE, 156 | description="Remove an item from your pocket pick database by ID", 157 | inputSchema=PocketRemove.schema(), 158 | ), 159 | Tool( 160 | name=PocketTools.GET, 161 | description="Get an item from your pocket pick database by ID", 162 | inputSchema=PocketGet.schema(), 163 | ), 164 | Tool( 165 | name=PocketTools.BACKUP, 166 | description="Backup your pocket pick database to a specified location", 167 | inputSchema=PocketBackup.schema(), 168 | ), 169 | Tool( 170 | name=PocketTools.TO_FILE_BY_ID, 171 | description="Write a pocket pick item's content to a file by its ID (requires absolute file path)", 172 | inputSchema=PocketToFileById.schema(), 173 | ), 174 | ] 175 | 176 | @server.call_tool() 177 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 178 | # Override db_path if provided via command line 179 | if sqlite_database is not None: 180 | arguments["db"] = str(sqlite_database) 181 | elif "db" not in arguments: 182 | # Use default if not specified 183 | arguments["db"] = str(DEFAULT_SQLITE_DATABASE_PATH) 184 | 185 | db_path = Path(arguments["db"]) 186 | 187 | # Ensure the database exists and is initialized for every command 188 | from .modules.init_db import init_db 189 | connection = init_db(db_path) 190 | connection.close() 191 | 192 | match name: 193 | case PocketTools.ADD: 194 | command = AddCommand( 195 | id=arguments["id"], 196 | text=arguments["text"], 197 | tags=arguments.get("tags", []), 198 | db_path=db_path 199 | ) 200 | result = add(command) 201 | return [TextContent( 202 | type="text", 203 | text=f"Added item with ID: {result.id}\nText: {result.text}\nTags: {', '.join(result.tags)}" 204 | )] 205 | 206 | case PocketTools.ADD_FILE: 207 | command = AddFileCommand( 208 | id=arguments["id"], 209 | file_path=arguments["file_path"], 210 | tags=arguments.get("tags", []), 211 | db_path=db_path 212 | ) 213 | result = add_file(command) 214 | return [TextContent( 215 | type="text", 216 | text=f"Added file content with ID: {result.id}\nFrom file: {arguments['file_path']}\nTags: {', '.join(result.tags)}" 217 | )] 218 | 219 | case PocketTools.FIND: 220 | command = FindCommand( 221 | text=arguments["text"], 222 | mode=arguments.get("mode", "substr"), 223 | limit=arguments.get("limit", 5), 224 | info=arguments.get("info", False), 225 | tags=arguments.get("tags", []), 226 | db_path=db_path 227 | ) 228 | results = find(command) 229 | 230 | if not results: 231 | return [TextContent( 232 | type="text", 233 | text="No items found matching your search criteria." 234 | )] 235 | 236 | output = [] 237 | for item in results: 238 | if command.info: 239 | output.append(f"ID: {item.id}") 240 | output.append(f"Created: {item.created.isoformat()}") 241 | output.append(f"Tags: {', '.join(item.tags)}") 242 | output.append(f"Text: {item.text}") 243 | output.append("") 244 | else: 245 | output.append(item.text) 246 | output.append("") 247 | 248 | return [TextContent( 249 | type="text", 250 | text="\n".join(output).strip() 251 | )] 252 | 253 | case PocketTools.LIST: 254 | command = ListCommand( 255 | tags=arguments.get("tags", []), 256 | limit=arguments.get("limit", 100), 257 | db_path=db_path 258 | ) 259 | results = list_items(command) 260 | 261 | if not results: 262 | return [TextContent( 263 | type="text", 264 | text="No items found." 265 | )] 266 | 267 | output = [] 268 | for item in results: 269 | output.append(f"ID: {item.id}") 270 | output.append(f"Created: {item.created.isoformat()}") 271 | output.append(f"Tags: {', '.join(item.tags)}") 272 | output.append(f"Text: {item.text}") 273 | output.append("") 274 | 275 | return [TextContent( 276 | type="text", 277 | text="\n".join(output).strip() 278 | )] 279 | 280 | case PocketTools.LIST_TAGS: 281 | command = ListTagsCommand( 282 | limit=arguments.get("limit", 1000), 283 | db_path=db_path 284 | ) 285 | results = list_tags(command) 286 | 287 | if not results: 288 | return [TextContent( 289 | type="text", 290 | text="No tags found." 291 | )] 292 | 293 | output = ["Tags:"] 294 | for item in results: 295 | output.append(f"{item['tag']} ({item['count']})") 296 | 297 | return [TextContent( 298 | type="text", 299 | text="\n".join(output) 300 | )] 301 | 302 | case PocketTools.LIST_IDS: 303 | command = ListIdsCommand( 304 | tags=arguments.get("tags", []), 305 | limit=arguments.get("limit", 100), 306 | db_path=db_path 307 | ) 308 | results = list_ids(command) 309 | 310 | if not results: 311 | return [TextContent( 312 | type="text", 313 | text="No item IDs found." 314 | )] 315 | 316 | return [TextContent( 317 | type="text", 318 | text="\n".join(results) 319 | )] 320 | 321 | case PocketTools.REMOVE: 322 | command = RemoveCommand( 323 | id=arguments["id"], 324 | db_path=db_path 325 | ) 326 | result = remove(command) 327 | 328 | if result: 329 | return [TextContent( 330 | type="text", 331 | text=f"Item {command.id} removed successfully." 332 | )] 333 | else: 334 | return [TextContent( 335 | type="text", 336 | text=f"Item {command.id} not found." 337 | )] 338 | 339 | case PocketTools.GET: 340 | command = GetCommand( 341 | id=arguments["id"], 342 | db_path=db_path 343 | ) 344 | result = get(command) 345 | 346 | if result: 347 | return [TextContent( 348 | type="text", 349 | text=f"ID: {result.id}\nCreated: {result.created.isoformat()}\nTags: {', '.join(result.tags)}\nText: {result.text}" 350 | )] 351 | else: 352 | return [TextContent( 353 | type="text", 354 | text=f"Item {command.id} not found." 355 | )] 356 | 357 | case PocketTools.BACKUP: 358 | command = BackupCommand( 359 | backup_path=Path(arguments["backup_path"]), 360 | db_path=db_path 361 | ) 362 | result = backup(command) 363 | 364 | if result: 365 | return [TextContent( 366 | type="text", 367 | text=f"Database backed up successfully to {command.backup_path}" 368 | )] 369 | else: 370 | return [TextContent( 371 | type="text", 372 | text=f"Failed to backup database to {command.backup_path}" 373 | )] 374 | 375 | case PocketTools.TO_FILE_BY_ID: 376 | command = ToFileByIdCommand( 377 | id=arguments["id"], 378 | output_file_path_abs=Path(arguments["output_file_path_abs"]), 379 | db_path=db_path 380 | ) 381 | result = to_file_by_id(command) 382 | 383 | if result: 384 | return [TextContent( 385 | type="text", 386 | text=f"Content written successfully to {command.output_file_path_abs}" 387 | )] 388 | else: 389 | return [TextContent( 390 | type="text", 391 | text=f"Failed to write content to {command.output_file_path_abs}" 392 | )] 393 | 394 | case _: 395 | raise ValueError(f"Unknown tool: {name}") 396 | 397 | options = server.create_initialization_options() 398 | async with stdio_server() as (read_stream, write_stream): 399 | await server.run(read_stream, write_stream, options, raise_exceptions=True) -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Tests package initialization -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/__init__.py: -------------------------------------------------------------------------------- 1 | # Functionality tests package initialization -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_add.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import json 6 | import sqlite3 7 | from ...modules.data_types import AddCommand, PocketItem 8 | from ...modules.functionality.add import add 9 | 10 | @pytest.fixture 11 | def temp_db_path(): 12 | # Create a temporary file path 13 | fd, path = tempfile.mkstemp() 14 | os.close(fd) 15 | 16 | # Return the path as a Path object 17 | yield Path(path) 18 | 19 | # Clean up the temp file after test 20 | if os.path.exists(path): 21 | os.unlink(path) 22 | 23 | def test_add_simple(temp_db_path): 24 | # Create a command to add a simple item 25 | command = AddCommand( 26 | id="test-item-1", 27 | text="This is a test item", 28 | tags=["test", "example"], 29 | db_path=temp_db_path 30 | ) 31 | 32 | # Add the item 33 | result = add(command) 34 | 35 | # Verify result is a PocketItem 36 | assert isinstance(result, PocketItem) 37 | assert result.text == "This is a test item" 38 | assert result.tags == ["test", "example"] 39 | assert result.id == "test-item-1" 40 | 41 | # Verify item was added to the database 42 | db = sqlite3.connect(temp_db_path) 43 | cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") 44 | row = cursor.fetchone() 45 | 46 | assert row is not None 47 | assert row[0] == "test-item-1" 48 | assert row[1] == "This is a test item" 49 | 50 | # Verify tags were stored as JSON 51 | stored_tags = json.loads(row[2]) 52 | assert stored_tags == ["test", "example"] 53 | 54 | # Verify no more rows exist 55 | assert cursor.fetchone() is None 56 | 57 | db.close() 58 | 59 | def test_add_with_tag_normalization(temp_db_path): 60 | # Create a command with tags that need normalization 61 | command = AddCommand( 62 | id="test-normalize", 63 | text="Item with tags to normalize", 64 | tags=["TAG", "with space", "under_score"], 65 | db_path=temp_db_path 66 | ) 67 | 68 | # Add the item 69 | result = add(command) 70 | 71 | # Verify tags were normalized 72 | assert result.tags == ["tag", "with-space", "under-score"] 73 | assert result.id == "test-normalize" 74 | 75 | # Verify in database 76 | db = sqlite3.connect(temp_db_path) 77 | cursor = db.execute("SELECT id, tags FROM POCKET_PICK") 78 | row = cursor.fetchone() 79 | 80 | assert row[0] == "test-normalize" 81 | stored_tags = json.loads(row[1]) 82 | assert stored_tags == ["tag", "with-space", "under-score"] 83 | 84 | db.close() 85 | 86 | def test_add_duplicate_id(temp_db_path): 87 | # Create first item 88 | command1 = AddCommand( 89 | id="duplicate-id", 90 | text="First item with this ID", 91 | tags=["test"], 92 | db_path=temp_db_path 93 | ) 94 | 95 | # Add the first item successfully 96 | result = add(command1) 97 | assert result.id == "duplicate-id" 98 | 99 | # Create second item with same ID 100 | command2 = AddCommand( 101 | id="duplicate-id", 102 | text="Second item with same ID", 103 | tags=["test"], 104 | db_path=temp_db_path 105 | ) 106 | 107 | # Attempt to add with the same ID should raise IntegrityError 108 | with pytest.raises(sqlite3.IntegrityError): 109 | add(command2) 110 | 111 | # Verify only the first item exists in the database 112 | db = sqlite3.connect(temp_db_path) 113 | cursor = db.execute("SELECT COUNT(*) FROM POCKET_PICK") 114 | count = cursor.fetchone()[0] 115 | assert count == 1 116 | 117 | cursor = db.execute("SELECT text FROM POCKET_PICK WHERE id = ?", ("duplicate-id",)) 118 | text = cursor.fetchone()[0] 119 | assert text == "First item with this ID" 120 | 121 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_add_file.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import json 6 | import sqlite3 7 | from ...modules.data_types import AddFileCommand, PocketItem 8 | from ...modules.functionality.add_file import add_file 9 | 10 | @pytest.fixture 11 | def temp_db_path(): 12 | # Create a temporary file path 13 | fd, path = tempfile.mkstemp() 14 | os.close(fd) 15 | 16 | # Return the path as a Path object 17 | yield Path(path) 18 | 19 | # Clean up the temp file after test 20 | if os.path.exists(path): 21 | os.unlink(path) 22 | 23 | @pytest.fixture 24 | def temp_file_with_content(): 25 | # Create a temporary file with content 26 | fd, path = tempfile.mkstemp() 27 | with os.fdopen(fd, 'w') as file: 28 | file.write("This is test content from a file") 29 | 30 | # Return the path as a string 31 | yield path 32 | 33 | # Clean up the temp file after test 34 | if os.path.exists(path): 35 | os.unlink(path) 36 | 37 | def test_add_file_simple(temp_db_path, temp_file_with_content): 38 | # Create a command to add a file content 39 | command = AddFileCommand( 40 | id="test-file-1", 41 | file_path=temp_file_with_content, 42 | tags=["test", "file"], 43 | db_path=temp_db_path 44 | ) 45 | 46 | # Add the item 47 | result = add_file(command) 48 | 49 | # Verify result is a PocketItem 50 | assert isinstance(result, PocketItem) 51 | assert result.text == "This is test content from a file" 52 | assert result.tags == ["test", "file"] 53 | assert result.id == "test-file-1" 54 | 55 | # Verify item was added to the database 56 | db = sqlite3.connect(temp_db_path) 57 | cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") 58 | row = cursor.fetchone() 59 | 60 | assert row is not None 61 | assert row[0] == "test-file-1" 62 | assert row[1] == "This is test content from a file" 63 | 64 | # Verify tags were stored as JSON 65 | stored_tags = json.loads(row[2]) 66 | assert stored_tags == ["test", "file"] 67 | 68 | # Verify no more rows exist 69 | assert cursor.fetchone() is None 70 | 71 | db.close() 72 | 73 | def test_add_file_with_tag_normalization(temp_db_path, temp_file_with_content): 74 | # Create a command with tags that need normalization 75 | command = AddFileCommand( 76 | id="test-file-normalize", 77 | file_path=temp_file_with_content, 78 | tags=["FILE", "with space", "under_score"], 79 | db_path=temp_db_path 80 | ) 81 | 82 | # Add the item 83 | result = add_file(command) 84 | 85 | # Verify tags were normalized 86 | assert result.tags == ["file", "with-space", "under-score"] 87 | assert result.id == "test-file-normalize" 88 | 89 | # Verify in database 90 | db = sqlite3.connect(temp_db_path) 91 | cursor = db.execute("SELECT id, tags FROM POCKET_PICK") 92 | row = cursor.fetchone() 93 | 94 | assert row[0] == "test-file-normalize" 95 | stored_tags = json.loads(row[1]) 96 | assert stored_tags == ["file", "with-space", "under-score"] 97 | 98 | db.close() 99 | 100 | def test_add_file_nonexistent(temp_db_path): 101 | # Create a command with a nonexistent file 102 | command = AddFileCommand( 103 | id="test-nonexistent", 104 | file_path="/nonexistent/file/path.txt", 105 | tags=["test"], 106 | db_path=temp_db_path 107 | ) 108 | 109 | # Expect FileNotFoundError when adding 110 | with pytest.raises(FileNotFoundError): 111 | add_file(command) 112 | 113 | def test_add_file_duplicate_id(temp_db_path, temp_file_with_content): 114 | # Create first file item 115 | command1 = AddFileCommand( 116 | id="duplicate-file-id", 117 | file_path=temp_file_with_content, 118 | tags=["test"], 119 | db_path=temp_db_path 120 | ) 121 | 122 | # Add the first item successfully 123 | result = add_file(command1) 124 | assert result.id == "duplicate-file-id" 125 | 126 | # Create second item with same ID 127 | command2 = AddFileCommand( 128 | id="duplicate-file-id", 129 | file_path=temp_file_with_content, 130 | tags=["different"], 131 | db_path=temp_db_path 132 | ) 133 | 134 | # Attempt to add with the same ID should raise IntegrityError 135 | with pytest.raises(sqlite3.IntegrityError): 136 | add_file(command2) 137 | 138 | # Verify only the first item exists in the database 139 | db = sqlite3.connect(temp_db_path) 140 | cursor = db.execute("SELECT COUNT(*) FROM POCKET_PICK") 141 | count = cursor.fetchone()[0] 142 | assert count == 1 143 | 144 | cursor = db.execute("SELECT tags FROM POCKET_PICK WHERE id = ?", ("duplicate-file-id",)) 145 | tags = json.loads(cursor.fetchone()[0]) 146 | assert tags == ["test"] # Original tags are preserved 147 | 148 | db.close() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_backup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | import sqlite3 5 | from pathlib import Path 6 | from ...modules.data_types import AddCommand, BackupCommand 7 | from ...modules.functionality.add import add 8 | from ...modules.functionality.backup import backup 9 | 10 | @pytest.fixture 11 | def temp_db_path(): 12 | # Create a temporary file path 13 | fd, path = tempfile.mkstemp() 14 | os.close(fd) 15 | 16 | # Return the path as a Path object 17 | yield Path(path) 18 | 19 | # Clean up the temp file after test 20 | if os.path.exists(path): 21 | os.unlink(path) 22 | 23 | @pytest.fixture 24 | def temp_backup_path(): 25 | # Create a temporary file path for backup 26 | fd, path = tempfile.mkstemp() 27 | os.close(fd) 28 | os.unlink(path) # Remove the file so backup can create it 29 | 30 | # Return the path as a Path object 31 | yield Path(path) 32 | 33 | # Clean up the temp file after test 34 | if os.path.exists(path): 35 | os.unlink(path) 36 | 37 | @pytest.fixture 38 | def populated_db(temp_db_path): 39 | # Add a test item to the database 40 | command = AddCommand( 41 | id="test-backup-item", 42 | text="Test item for backup", 43 | tags=["test", "backup"], 44 | db_path=temp_db_path 45 | ) 46 | 47 | add(command) 48 | return temp_db_path 49 | 50 | def test_backup_success(populated_db, temp_backup_path): 51 | # Backup the database 52 | command = BackupCommand( 53 | backup_path=temp_backup_path, 54 | db_path=populated_db 55 | ) 56 | 57 | result = backup(command) 58 | 59 | # Should return True indicating success 60 | assert result is True 61 | 62 | # Verify backup file exists 63 | assert temp_backup_path.exists() 64 | 65 | # Verify backup contains the same data as original 66 | original_db = sqlite3.connect(populated_db) 67 | original_cursor = original_db.execute("SELECT id, text, tags FROM POCKET_PICK") 68 | original_row = original_cursor.fetchone() 69 | original_db.close() 70 | 71 | backup_db = sqlite3.connect(temp_backup_path) 72 | backup_cursor = backup_db.execute("SELECT id, text, tags FROM POCKET_PICK") 73 | backup_row = backup_cursor.fetchone() 74 | backup_db.close() 75 | 76 | assert backup_row is not None 77 | assert backup_row[0] == original_row[0] # ID 78 | assert backup_row[1] == original_row[1] # text 79 | assert backup_row[2] == original_row[2] # tags 80 | 81 | def test_backup_nested_directory_creation(populated_db): 82 | # Create a backup path in a nested directory that doesn't exist 83 | with tempfile.TemporaryDirectory() as temp_dir: 84 | nested_dir = Path(temp_dir) / "nested" / "dirs" 85 | backup_path = nested_dir / "backup.db" 86 | 87 | # Backup the database 88 | command = BackupCommand( 89 | backup_path=backup_path, 90 | db_path=populated_db 91 | ) 92 | 93 | result = backup(command) 94 | 95 | # Should return True indicating success 96 | assert result is True 97 | 98 | # Verify backup file exists 99 | assert backup_path.exists() 100 | 101 | def test_backup_from_nonexistent_db(temp_db_path, temp_backup_path): 102 | # Try to backup from a nonexistent database 103 | # (temp_db_path fixture exists but is empty) 104 | command = BackupCommand( 105 | backup_path=temp_backup_path, 106 | db_path=temp_db_path 107 | ) 108 | 109 | result = backup(command) 110 | 111 | # Should return True indicating success (empty database created and backed up) 112 | assert result is True 113 | assert temp_backup_path.exists() -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_find.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import json 6 | import sqlite3 7 | from datetime import datetime 8 | from ...modules.data_types import AddCommand, FindCommand, PocketItem 9 | from ...modules.functionality.add import add 10 | from ...modules.functionality.find import find 11 | from ...modules.init_db import init_db 12 | 13 | @pytest.fixture 14 | def temp_db_path(): 15 | # Create a temporary file path 16 | fd, path = tempfile.mkstemp() 17 | os.close(fd) 18 | 19 | # Return the path as a Path object 20 | yield Path(path) 21 | 22 | # Clean up the temp file after test 23 | if os.path.exists(path): 24 | os.unlink(path) 25 | 26 | @pytest.fixture 27 | def populated_db(temp_db_path): 28 | # Create sample items 29 | items = [ 30 | {"id": "python-1", "text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 31 | {"id": "sql-1", "text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 32 | {"id": "testing-1", "text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 33 | {"id": "regex-1", "text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 34 | {"id": "learning-1", "text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 35 | ] 36 | 37 | # Add items to the database 38 | for item in items: 39 | command = AddCommand( 40 | id=item["id"], 41 | text=item["text"], 42 | tags=item["tags"], 43 | db_path=temp_db_path 44 | ) 45 | add(command) 46 | 47 | return temp_db_path 48 | 49 | def test_find_substr(populated_db): 50 | # Search for "programming" substring 51 | command = FindCommand( 52 | text="programming", 53 | mode="substr", 54 | limit=10, 55 | db_path=populated_db 56 | ) 57 | 58 | results = find(command) 59 | 60 | # Should match "Python programming is fun" 61 | assert len(results) == 1 62 | assert "Python programming is fun" in [r.text for r in results] 63 | 64 | def test_find_fts(populated_db): 65 | # Test basic FTS search with a single word 66 | command = FindCommand( 67 | text="SQL", 68 | mode="fts", 69 | limit=10, 70 | db_path=populated_db 71 | ) 72 | 73 | results = find(command) 74 | 75 | # Should match "SQL databases are powerful" 76 | assert len(results) == 1 77 | assert "SQL databases are powerful" in [r.text for r in results] 78 | 79 | def test_find_fts_phrase(populated_db): 80 | # Test FTS with a phrase (multiple words in exact order) 81 | command = FindCommand( 82 | text="Regular expressions", 83 | mode="fts", 84 | limit=10, 85 | db_path=populated_db 86 | ) 87 | 88 | results = find(command) 89 | 90 | # Should match "Regular expressions can be complex" 91 | assert len(results) == 1 92 | assert "Regular expressions can be complex" in [r.text for r in results] 93 | 94 | def test_find_fts_multi_term(populated_db): 95 | # Test FTS with multiple terms (not necessarily in order) 96 | command = FindCommand( 97 | text="programming fun", 98 | mode="fts", 99 | limit=10, 100 | db_path=populated_db 101 | ) 102 | 103 | results = find(command) 104 | 105 | # Should match items containing both "programming" and "fun" 106 | assert len(results) > 0 107 | 108 | # Check that all results contain both "programming" AND "fun" 109 | for result in results: 110 | assert "programming" in result.text.lower() and "fun" in result.text.lower() 111 | 112 | def test_find_fts_with_tags(populated_db): 113 | # Test FTS with tag filtering 114 | command = FindCommand( 115 | text="programming", 116 | mode="fts", 117 | tags=["fun"], # Only items tagged with "fun" 118 | limit=10, 119 | db_path=populated_db 120 | ) 121 | 122 | results = find(command) 123 | 124 | # Should match items containing "programming" AND tagged with "fun" 125 | assert len(results) == 1 126 | assert "Python programming is fun" in [r.text for r in results] 127 | 128 | def test_find_fts_exact_phrase(populated_db): 129 | """ 130 | Test exact phrase matching functionality. 131 | 132 | This test is simplified to focus on the core functionality without relying 133 | on specific matching patterns that might be hard to reproduce with FTS5. 134 | """ 135 | # First make sure we have a known item with a specific phrase 136 | command = AddCommand( 137 | id="test-exact-phrase-1", 138 | text="This contains programming fun as a phrase", 139 | tags=["test", "phrase"], 140 | db_path=populated_db 141 | ) 142 | result1 = add(command) 143 | 144 | # Add an item with same words but in reverse order 145 | command = AddCommand( 146 | id="test-exact-phrase-2", 147 | text="This has fun programming in reverse order", 148 | tags=["test", "reverse"], 149 | db_path=populated_db 150 | ) 151 | result2 = add(command) 152 | 153 | # Search using quoted exact phrase matching 154 | command = FindCommand( 155 | text='"programming fun"', # The quotes force exact phrase matching in FTS5 156 | mode="fts", 157 | limit=10, 158 | db_path=populated_db 159 | ) 160 | 161 | results = find(command) 162 | 163 | # Verify that our item with the exact phrase is found 164 | # And the item with reversed words is not found 165 | found_exact = "This contains programming fun as a phrase" in [r.text for r in results] 166 | found_reverse = "This has fun programming in reverse order" in [r.text for r in results] 167 | 168 | assert found_exact, "Should find item with exact phrase" 169 | assert not found_reverse, "Should not find item with reverse word order" 170 | 171 | def test_find_glob(populated_db): 172 | # Search for text starting with "Test" 173 | command = FindCommand( 174 | text="Test*", 175 | mode="glob", 176 | limit=10, 177 | db_path=populated_db 178 | ) 179 | 180 | results = find(command) 181 | 182 | # Should match "Testing code is important" 183 | assert len(results) == 1 184 | assert "Testing code is important" in [r.text for r in results] 185 | 186 | def test_find_regex(populated_db): 187 | # Search for text containing "regular" (case insensitive) 188 | command = FindCommand( 189 | text=".*regular.*", 190 | mode="regex", 191 | limit=10, 192 | db_path=populated_db 193 | ) 194 | 195 | results = find(command) 196 | 197 | # Should match "Regular expressions can be complex" 198 | assert len(results) == 1 199 | assert "Regular expressions can be complex" in [r.text for r in results] 200 | 201 | def test_find_exact(populated_db): 202 | # Search for exact match 203 | command = FindCommand( 204 | text="Learning new technologies is exciting", 205 | mode="exact", 206 | limit=10, 207 | db_path=populated_db 208 | ) 209 | 210 | results = find(command) 211 | 212 | # Should match exactly one item 213 | assert len(results) == 1 214 | assert results[0].text == "Learning new technologies is exciting" 215 | 216 | def test_find_with_tags(populated_db): 217 | # Search for items with specific tags 218 | command = FindCommand( 219 | text="", # No text search 220 | tags=["fun"], 221 | limit=10, 222 | db_path=populated_db 223 | ) 224 | 225 | results = find(command) 226 | 227 | # Should match items with the "fun" tag 228 | assert len(results) == 2 229 | assert "Python programming is fun" in [r.text for r in results] 230 | assert "Learning new technologies is exciting" in [r.text for r in results] 231 | 232 | def test_find_with_text_and_tags(populated_db): 233 | # Search for items with specific text and tags 234 | command = FindCommand( 235 | text="programming", 236 | mode="substr", 237 | tags=["fun"], 238 | limit=10, 239 | db_path=populated_db 240 | ) 241 | 242 | results = find(command) 243 | 244 | # Should match items with "programming" text and "fun" tag 245 | assert len(results) == 1 246 | assert "Python programming is fun" in [r.text for r in results] 247 | 248 | def test_find_limit(populated_db): 249 | # Search with limit 250 | command = FindCommand( 251 | text="", # Match all 252 | limit=2, 253 | db_path=populated_db 254 | ) 255 | 256 | results = find(command) 257 | 258 | # Should only return 2 items (due to limit) 259 | assert len(results) == 2 -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | from ...modules.data_types import AddCommand, ListCommand 6 | from ...modules.functionality.add import add 7 | from ...modules.functionality.list import list_items 8 | 9 | @pytest.fixture 10 | def temp_db_path(): 11 | # Create a temporary file path 12 | fd, path = tempfile.mkstemp() 13 | os.close(fd) 14 | 15 | # Return the path as a Path object 16 | yield Path(path) 17 | 18 | # Clean up the temp file after test 19 | if os.path.exists(path): 20 | os.unlink(path) 21 | 22 | @pytest.fixture 23 | def populated_db(temp_db_path): 24 | # Create sample items 25 | items = [ 26 | {"id": "python-1", "text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 27 | {"id": "sql-1", "text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 28 | {"id": "testing-1", "text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 29 | {"id": "regex-1", "text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 30 | {"id": "learning-1", "text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 31 | ] 32 | 33 | # Add items to the database 34 | for item in items: 35 | command = AddCommand( 36 | id=item["id"], 37 | text=item["text"], 38 | tags=item["tags"], 39 | db_path=temp_db_path 40 | ) 41 | add(command) 42 | 43 | return temp_db_path 44 | 45 | def test_list_all(populated_db): 46 | # List all items 47 | command = ListCommand( 48 | limit=10, 49 | db_path=populated_db 50 | ) 51 | 52 | results = list_items(command) 53 | 54 | # Should return all 5 items 55 | assert len(results) == 5 56 | 57 | # Check that all expected texts are present 58 | texts = [result.text for result in results] 59 | expected_texts = [ 60 | "Python programming is fun", 61 | "SQL databases are powerful", 62 | "Testing code is important", 63 | "Regular expressions can be complex", 64 | "Learning new technologies is exciting" 65 | ] 66 | 67 | for expected in expected_texts: 68 | assert expected in texts 69 | 70 | def test_list_with_tags(populated_db): 71 | # List items with specific tag 72 | command = ListCommand( 73 | tags=["programming"], 74 | limit=10, 75 | db_path=populated_db 76 | ) 77 | 78 | results = list_items(command) 79 | 80 | # Should return items with the "programming" tag (4 items) 81 | assert len(results) == 4 82 | 83 | # Verify the correct items are returned 84 | texts = [result.text for result in results] 85 | expected_texts = [ 86 | "Python programming is fun", 87 | "SQL databases are powerful", 88 | "Testing code is important", 89 | "Regular expressions can be complex" 90 | ] 91 | 92 | for expected in expected_texts: 93 | assert expected in texts 94 | 95 | def test_list_with_multiple_tags(populated_db): 96 | # List items with multiple tags 97 | command = ListCommand( 98 | tags=["programming", "fun"], 99 | limit=10, 100 | db_path=populated_db 101 | ) 102 | 103 | results = list_items(command) 104 | 105 | # Should return items with both "programming" and "fun" tags (1 item) 106 | assert len(results) == 1 107 | assert results[0].text == "Python programming is fun" 108 | 109 | def test_list_limit(populated_db): 110 | # List with limit 111 | command = ListCommand( 112 | limit=2, 113 | db_path=populated_db 114 | ) 115 | 116 | results = list_items(command) 117 | 118 | # Should only return 2 items 119 | assert len(results) == 2 -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_list_ids.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | from ...modules.data_types import AddCommand, ListIdsCommand 6 | from ...modules.functionality.add import add 7 | from ...modules.functionality.list_ids import list_ids 8 | 9 | @pytest.fixture 10 | def temp_db_path(): 11 | fd, path = tempfile.mkstemp() 12 | os.close(fd) 13 | yield Path(path) 14 | if os.path.exists(path): 15 | os.unlink(path) 16 | 17 | @pytest.fixture 18 | def populated_db(temp_db_path): 19 | items = [ 20 | {"id": "python-1", "text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 21 | {"id": "sql-1", "text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 22 | {"id": "testing-1", "text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 23 | {"id": "regex-1", "text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 24 | {"id": "learning-1", "text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]}, 25 | ] 26 | 27 | for item in items: 28 | command = AddCommand( 29 | id=item["id"], 30 | text=item["text"], 31 | tags=item["tags"], 32 | db_path=temp_db_path, 33 | ) 34 | add(command) 35 | 36 | return temp_db_path 37 | 38 | def test_list_ids_all(populated_db): 39 | command = ListIdsCommand( 40 | limit=10, 41 | db_path=populated_db, 42 | ) 43 | 44 | results = list_ids(command) 45 | 46 | assert len(results) == 5 47 | for expected in [ 48 | "python-1", 49 | "sql-1", 50 | "testing-1", 51 | "regex-1", 52 | "learning-1", 53 | ]: 54 | assert expected in results 55 | 56 | def test_list_ids_with_tags(populated_db): 57 | command = ListIdsCommand( 58 | tags=["programming"], 59 | limit=10, 60 | db_path=populated_db, 61 | ) 62 | 63 | results = list_ids(command) 64 | 65 | assert len(results) == 4 66 | for expected in ["python-1", "sql-1", "testing-1", "regex-1"]: 67 | assert expected in results 68 | 69 | def test_list_ids_limit(populated_db): 70 | command = ListIdsCommand( 71 | limit=2, 72 | db_path=populated_db, 73 | ) 74 | 75 | results = list_ids(command) 76 | 77 | assert len(results) == 2 78 | 79 | def test_list_ids_empty_db(temp_db_path): 80 | command = ListIdsCommand( 81 | db_path=temp_db_path, 82 | ) 83 | 84 | results = list_ids(command) 85 | 86 | assert len(results) == 0 87 | -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_list_tags.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | from ...modules.data_types import AddCommand, ListTagsCommand 6 | from ...modules.functionality.add import add 7 | from ...modules.functionality.list_tags import list_tags 8 | 9 | @pytest.fixture 10 | def temp_db_path(): 11 | # Create a temporary file path 12 | fd, path = tempfile.mkstemp() 13 | os.close(fd) 14 | 15 | # Return the path as a Path object 16 | yield Path(path) 17 | 18 | # Clean up the temp file after test 19 | if os.path.exists(path): 20 | os.unlink(path) 21 | 22 | @pytest.fixture 23 | def populated_db(temp_db_path): 24 | # Create sample items 25 | items = [ 26 | {"id": "python-1", "text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 27 | {"id": "sql-1", "text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 28 | {"id": "testing-1", "text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 29 | {"id": "regex-1", "text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 30 | {"id": "learning-1", "text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 31 | ] 32 | 33 | # Add items to the database 34 | for item in items: 35 | command = AddCommand( 36 | id=item["id"], 37 | text=item["text"], 38 | tags=item["tags"], 39 | db_path=temp_db_path 40 | ) 41 | add(command) 42 | 43 | return temp_db_path 44 | 45 | def test_list_tags_all(populated_db): 46 | # List all tags 47 | command = ListTagsCommand( 48 | db_path=populated_db 49 | ) 50 | 51 | results = list_tags(command) 52 | 53 | # Verify all expected tags are present 54 | tags = [result["tag"] for result in results] 55 | expected_tags = [ 56 | "programming", # Count: 4 57 | "fun", # Count: 2 58 | "python", # Count: 1 59 | "sql", # Count: 1 60 | "database", # Count: 1 61 | "testing", # Count: 1 62 | "code", # Count: 1 63 | "regex", # Count: 1 64 | "advanced", # Count: 1 65 | "learning", # Count: 1 66 | "technology" # Count: 1 67 | ] 68 | 69 | for expected in expected_tags: 70 | assert expected in tags 71 | 72 | # Verify the most common tag is first (sorted by count) 73 | assert results[0]["tag"] == "programming" 74 | assert results[0]["count"] == 4 75 | 76 | # Verify second most common tag 77 | assert results[1]["tag"] == "fun" 78 | assert results[1]["count"] == 2 79 | 80 | def test_list_tags_limit(populated_db): 81 | # List tags with limit 82 | command = ListTagsCommand( 83 | limit=3, 84 | db_path=populated_db 85 | ) 86 | 87 | results = list_tags(command) 88 | 89 | # Should only return 3 tags 90 | assert len(results) == 3 91 | 92 | # Verify the top 3 tags in order by count 93 | # (with ties broken alphabetically) 94 | assert results[0]["tag"] == "programming" 95 | assert results[1]["tag"] == "fun" 96 | 97 | # The third item should be one of the single-count tags 98 | assert results[2]["count"] == 1 99 | 100 | def test_list_tags_empty_db(temp_db_path): 101 | # List tags in empty database 102 | command = ListTagsCommand( 103 | db_path=temp_db_path 104 | ) 105 | 106 | results = list_tags(command) 107 | 108 | # Should return empty list 109 | assert len(results) == 0 -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_remove_get.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import sqlite3 6 | from ...modules.data_types import AddCommand, RemoveCommand, GetCommand 7 | from ...modules.functionality.add import add 8 | from ...modules.functionality.remove import remove 9 | from ...modules.functionality.get import get 10 | 11 | @pytest.fixture 12 | def temp_db_path(): 13 | # Create a temporary file path 14 | fd, path = tempfile.mkstemp() 15 | os.close(fd) 16 | 17 | # Return the path as a Path object 18 | yield Path(path) 19 | 20 | # Clean up the temp file after test 21 | if os.path.exists(path): 22 | os.unlink(path) 23 | 24 | @pytest.fixture 25 | def item_id(temp_db_path): 26 | # Add a test item and return its ID 27 | command = AddCommand( 28 | id="test-get-remove", 29 | text="Test item for get and remove", 30 | tags=["test", "example"], 31 | db_path=temp_db_path 32 | ) 33 | 34 | result = add(command) 35 | return result.id, temp_db_path 36 | 37 | def test_get_item(item_id): 38 | id, db_path = item_id 39 | 40 | # Get the item by ID 41 | command = GetCommand( 42 | id=id, 43 | db_path=db_path 44 | ) 45 | 46 | result = get(command) 47 | 48 | # Verify item properties 49 | assert result is not None 50 | assert result.id == id 51 | assert result.text == "Test item for get and remove" 52 | assert set(result.tags) == set(["test", "example"]) 53 | 54 | def test_get_nonexistent_item(temp_db_path): 55 | # Try to get a nonexistent item 56 | command = GetCommand( 57 | id="nonexistent-id", 58 | db_path=temp_db_path 59 | ) 60 | 61 | result = get(command) 62 | 63 | # Should return None 64 | assert result is None 65 | 66 | def test_remove_item(item_id): 67 | id, db_path = item_id 68 | 69 | # Remove the item 70 | command = RemoveCommand( 71 | id=id, 72 | db_path=db_path 73 | ) 74 | 75 | result = remove(command) 76 | 77 | # Should return True indicating success 78 | assert result is True 79 | 80 | # Verify item is no longer in the database 81 | db = sqlite3.connect(db_path) 82 | cursor = db.execute("SELECT COUNT(*) FROM POCKET_PICK WHERE id = ?", (id,)) 83 | count = cursor.fetchone()[0] 84 | db.close() 85 | 86 | assert count == 0 87 | 88 | # Trying to get the removed item should return None 89 | get_command = GetCommand( 90 | id=id, 91 | db_path=db_path 92 | ) 93 | get_result = get(get_command) 94 | assert get_result is None 95 | 96 | def test_remove_nonexistent_item(temp_db_path): 97 | # Try to remove a nonexistent item 98 | command = RemoveCommand( 99 | id="nonexistent-id", 100 | db_path=temp_db_path 101 | ) 102 | 103 | result = remove(command) 104 | 105 | # Should return False indicating failure 106 | assert result is False -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/functionality/test_to_file_by_id.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import json 6 | import sqlite3 7 | from ...modules.data_types import AddCommand, ToFileByIdCommand, PocketItem 8 | from ...modules.functionality.add import add 9 | from ...modules.functionality.to_file_by_id import to_file_by_id 10 | 11 | @pytest.fixture 12 | def temp_db_path(): 13 | # Create a temporary file path 14 | fd, path = tempfile.mkstemp() 15 | os.close(fd) 16 | 17 | # Return the path as a Path object 18 | yield Path(path) 19 | 20 | # Clean up the temp file after test 21 | if os.path.exists(path): 22 | os.unlink(path) 23 | 24 | @pytest.fixture 25 | def sample_item(temp_db_path): 26 | # Add a sample item to the database and return it 27 | command = AddCommand( 28 | id="test-file-output", 29 | text="This is sample content for testing to_file_by_id function", 30 | tags=["test", "sample"], 31 | db_path=temp_db_path 32 | ) 33 | 34 | return add(command) 35 | 36 | def test_to_file_by_id_successful(temp_db_path, sample_item): 37 | # Create a temporary output file path 38 | fd, output_path = tempfile.mkstemp() 39 | os.close(fd) 40 | os.unlink(output_path) # Remove the file so we can test creation 41 | 42 | try: 43 | # Create command to write content to file 44 | command = ToFileByIdCommand( 45 | id=sample_item.id, 46 | output_file_path_abs=output_path, 47 | db_path=temp_db_path 48 | ) 49 | 50 | # Write content to file 51 | result = to_file_by_id(command) 52 | 53 | # Verify result is True 54 | assert result is True 55 | 56 | # Verify file was created with correct content 57 | assert os.path.exists(output_path) 58 | with open(output_path, 'r', encoding='utf-8') as f: 59 | content = f.read() 60 | 61 | assert content == sample_item.text 62 | finally: 63 | # Clean up the temp file 64 | if os.path.exists(output_path): 65 | os.unlink(output_path) 66 | 67 | def test_to_file_by_id_nonexistent_id(temp_db_path): 68 | # Create a temporary output file path 69 | fd, output_path = tempfile.mkstemp() 70 | os.close(fd) 71 | os.unlink(output_path) # Remove the file so we can test creation 72 | 73 | try: 74 | # Create command with non-existent ID 75 | command = ToFileByIdCommand( 76 | id="nonexistent-id", 77 | output_file_path_abs=output_path, 78 | db_path=temp_db_path 79 | ) 80 | 81 | # Attempt to write content to file 82 | result = to_file_by_id(command) 83 | 84 | # Verify result is False 85 | assert result is False 86 | 87 | # Verify file was not created 88 | assert not os.path.exists(output_path) 89 | finally: 90 | # Clean up the temp file if it was created 91 | if os.path.exists(output_path): 92 | os.unlink(output_path) 93 | 94 | def test_to_file_by_id_creates_directories(temp_db_path, sample_item): 95 | # Create a temporary directory 96 | temp_dir = tempfile.mkdtemp() 97 | try: 98 | # Create a path with nested directories that don't exist 99 | output_path = os.path.join(temp_dir, "nested", "dirs", "output.txt") 100 | 101 | # Create command to write content to file 102 | command = ToFileByIdCommand( 103 | id=sample_item.id, 104 | output_file_path_abs=output_path, 105 | db_path=temp_db_path 106 | ) 107 | 108 | # Write content to file 109 | result = to_file_by_id(command) 110 | 111 | # Verify result is True 112 | assert result is True 113 | 114 | # Verify file was created with correct content 115 | assert os.path.exists(output_path) 116 | with open(output_path, 'r', encoding='utf-8') as f: 117 | content = f.read() 118 | 119 | assert content == sample_item.text 120 | finally: 121 | # Clean up the temp dir 122 | import shutil 123 | shutil.rmtree(temp_dir) 124 | 125 | def test_to_file_by_id_handles_errors(temp_db_path, sample_item, monkeypatch): 126 | # Mock the open function to raise a PermissionError 127 | def mock_open_with_permission_error(*args, **kwargs): 128 | raise PermissionError("Permission denied") 129 | 130 | # Create a temporary output file path 131 | fd, output_path = tempfile.mkstemp() 132 | os.close(fd) 133 | os.unlink(output_path) 134 | 135 | try: 136 | # Create command to write content to file 137 | command = ToFileByIdCommand( 138 | id=sample_item.id, 139 | output_file_path_abs=output_path, 140 | db_path=temp_db_path 141 | ) 142 | 143 | # Monkeypatch the built-in open function 144 | monkeypatch.setattr("builtins.open", mock_open_with_permission_error) 145 | 146 | # Attempt to write content to file 147 | result = to_file_by_id(command) 148 | 149 | # Verify result is False because of permission error 150 | assert result is False 151 | finally: 152 | # Clean up the temp file if it was created 153 | if os.path.exists(output_path): 154 | os.unlink(output_path) -------------------------------------------------------------------------------- /src/mcp_server_pocket_pick/tests/test_init_db.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | import os 4 | from pathlib import Path 5 | import sqlite3 6 | from ..modules.init_db import init_db, normalize_tag, normalize_tags 7 | 8 | def test_init_db(): 9 | # Create a temporary file path 10 | fd, path = tempfile.mkstemp() 11 | os.close(fd) 12 | 13 | try: 14 | # Initialize database with the temp path 15 | db_path = Path(path) 16 | db = init_db(db_path) 17 | 18 | # Verify connection is open 19 | assert isinstance(db, sqlite3.Connection) 20 | 21 | # Verify POCKET_PICK table exists 22 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='POCKET_PICK'") 23 | assert cursor.fetchone() is not None 24 | 25 | # Verify indexes exist 26 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_created'") 27 | assert cursor.fetchone() is not None 28 | 29 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_text'") 30 | assert cursor.fetchone() is not None 31 | 32 | # Close the connection 33 | db.close() 34 | finally: 35 | # Clean up the temp file 36 | if os.path.exists(path): 37 | os.unlink(path) 38 | 39 | def test_normalize_tag(): 40 | # Test lowercase conversion 41 | assert normalize_tag("TAG") == "tag" 42 | 43 | # Test whitespace trimming 44 | assert normalize_tag(" tag ") == "tag" 45 | 46 | # Test space replacement 47 | assert normalize_tag("my tag") == "my-tag" 48 | 49 | # Test underscore replacement 50 | assert normalize_tag("my_tag") == "my-tag" 51 | 52 | # Test combined operations 53 | assert normalize_tag(" MY_TAG with SPACES ") == "my-tag-with-spaces" 54 | 55 | def test_normalize_tags(): 56 | tags = ["TAG1", " tag2 ", "my_tag3", "My Tag4"] 57 | normalized = normalize_tags(tags) 58 | 59 | assert normalized == ["tag1", "tag2", "my-tag3", "my-tag4"] -------------------------------------------------------------------------------- /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.8.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/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 24 | wheels = [ 25 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2025.1.31" 31 | source = { registry = "https://pypi.org/simple" } 32 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 35 | ] 36 | 37 | [[package]] 38 | name = "click" 39 | version = "8.1.8" 40 | source = { registry = "https://pypi.org/simple" } 41 | dependencies = [ 42 | { name = "colorama", marker = "sys_platform == 'win32'" }, 43 | ] 44 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 45 | wheels = [ 46 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 47 | ] 48 | 49 | [[package]] 50 | name = "colorama" 51 | version = "0.4.6" 52 | source = { registry = "https://pypi.org/simple" } 53 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 54 | wheels = [ 55 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 56 | ] 57 | 58 | [[package]] 59 | name = "exceptiongroup" 60 | version = "1.2.2" 61 | source = { registry = "https://pypi.org/simple" } 62 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 63 | wheels = [ 64 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 65 | ] 66 | 67 | [[package]] 68 | name = "h11" 69 | version = "0.14.0" 70 | source = { registry = "https://pypi.org/simple" } 71 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 72 | wheels = [ 73 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 74 | ] 75 | 76 | [[package]] 77 | name = "httpcore" 78 | version = "1.0.7" 79 | source = { registry = "https://pypi.org/simple" } 80 | dependencies = [ 81 | { name = "certifi" }, 82 | { name = "h11" }, 83 | ] 84 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 85 | wheels = [ 86 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 87 | ] 88 | 89 | [[package]] 90 | name = "httpx" 91 | version = "0.28.1" 92 | source = { registry = "https://pypi.org/simple" } 93 | dependencies = [ 94 | { name = "anyio" }, 95 | { name = "certifi" }, 96 | { name = "httpcore" }, 97 | { name = "idna" }, 98 | ] 99 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 100 | wheels = [ 101 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 102 | ] 103 | 104 | [[package]] 105 | name = "httpx-sse" 106 | version = "0.4.0" 107 | source = { registry = "https://pypi.org/simple" } 108 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 109 | wheels = [ 110 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 111 | ] 112 | 113 | [[package]] 114 | name = "idna" 115 | version = "3.10" 116 | source = { registry = "https://pypi.org/simple" } 117 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 118 | wheels = [ 119 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 120 | ] 121 | 122 | [[package]] 123 | name = "iniconfig" 124 | version = "2.0.0" 125 | source = { registry = "https://pypi.org/simple" } 126 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 127 | wheels = [ 128 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 129 | ] 130 | 131 | [[package]] 132 | name = "mcp" 133 | version = "1.3.0" 134 | source = { registry = "https://pypi.org/simple" } 135 | dependencies = [ 136 | { name = "anyio" }, 137 | { name = "httpx" }, 138 | { name = "httpx-sse" }, 139 | { name = "pydantic" }, 140 | { name = "pydantic-settings" }, 141 | { name = "sse-starlette" }, 142 | { name = "starlette" }, 143 | { name = "uvicorn" }, 144 | ] 145 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 } 146 | wheels = [ 147 | { url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 }, 148 | ] 149 | 150 | [[package]] 151 | name = "mcp-server-pocket-pick" 152 | version = "0.1.0" 153 | source = { editable = "." } 154 | dependencies = [ 155 | { name = "click" }, 156 | { name = "mcp" }, 157 | { name = "pydantic" }, 158 | ] 159 | 160 | [package.dev-dependencies] 161 | dev = [ 162 | { name = "pytest" }, 163 | ] 164 | 165 | [package.metadata] 166 | requires-dist = [ 167 | { name = "click", specifier = ">=8.1.7" }, 168 | { name = "mcp", specifier = ">=1.0.0" }, 169 | { name = "pydantic", specifier = ">=2.0.0" }, 170 | ] 171 | 172 | [package.metadata.requires-dev] 173 | dev = [{ name = "pytest", specifier = ">=8.0.0" }] 174 | 175 | [[package]] 176 | name = "packaging" 177 | version = "24.2" 178 | source = { registry = "https://pypi.org/simple" } 179 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 182 | ] 183 | 184 | [[package]] 185 | name = "pluggy" 186 | version = "1.5.0" 187 | source = { registry = "https://pypi.org/simple" } 188 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 189 | wheels = [ 190 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 191 | ] 192 | 193 | [[package]] 194 | name = "pydantic" 195 | version = "2.10.6" 196 | source = { registry = "https://pypi.org/simple" } 197 | dependencies = [ 198 | { name = "annotated-types" }, 199 | { name = "pydantic-core" }, 200 | { name = "typing-extensions" }, 201 | ] 202 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 203 | wheels = [ 204 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 205 | ] 206 | 207 | [[package]] 208 | name = "pydantic-core" 209 | version = "2.27.2" 210 | source = { registry = "https://pypi.org/simple" } 211 | dependencies = [ 212 | { name = "typing-extensions" }, 213 | ] 214 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 215 | wheels = [ 216 | { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, 217 | { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, 218 | { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, 219 | { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, 220 | { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, 221 | { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, 222 | { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, 223 | { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, 224 | { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, 225 | { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, 226 | { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, 227 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 228 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 229 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 230 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 231 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 232 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 233 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 234 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 235 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 236 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 237 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 238 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 239 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 240 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 241 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 242 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 243 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 244 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 245 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 246 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 247 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 248 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 249 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 250 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 251 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 252 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 253 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 254 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 255 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 256 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 257 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 258 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 259 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 260 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 261 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 262 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 263 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 264 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 265 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 266 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 267 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 268 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 269 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 270 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 271 | { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, 272 | { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, 273 | { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, 274 | { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, 275 | { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, 276 | { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, 277 | { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, 278 | { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, 279 | { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, 280 | ] 281 | 282 | [[package]] 283 | name = "pydantic-settings" 284 | version = "2.8.1" 285 | source = { registry = "https://pypi.org/simple" } 286 | dependencies = [ 287 | { name = "pydantic" }, 288 | { name = "python-dotenv" }, 289 | ] 290 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 291 | wheels = [ 292 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 293 | ] 294 | 295 | [[package]] 296 | name = "pytest" 297 | version = "8.3.5" 298 | source = { registry = "https://pypi.org/simple" } 299 | dependencies = [ 300 | { name = "colorama", marker = "sys_platform == 'win32'" }, 301 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 302 | { name = "iniconfig" }, 303 | { name = "packaging" }, 304 | { name = "pluggy" }, 305 | { name = "tomli", marker = "python_full_version < '3.11'" }, 306 | ] 307 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 308 | wheels = [ 309 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 310 | ] 311 | 312 | [[package]] 313 | name = "python-dotenv" 314 | version = "1.0.1" 315 | source = { registry = "https://pypi.org/simple" } 316 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 317 | wheels = [ 318 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 319 | ] 320 | 321 | [[package]] 322 | name = "sniffio" 323 | version = "1.3.1" 324 | source = { registry = "https://pypi.org/simple" } 325 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 326 | wheels = [ 327 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 328 | ] 329 | 330 | [[package]] 331 | name = "sse-starlette" 332 | version = "2.2.1" 333 | source = { registry = "https://pypi.org/simple" } 334 | dependencies = [ 335 | { name = "anyio" }, 336 | { name = "starlette" }, 337 | ] 338 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 339 | wheels = [ 340 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 341 | ] 342 | 343 | [[package]] 344 | name = "starlette" 345 | version = "0.46.1" 346 | source = { registry = "https://pypi.org/simple" } 347 | dependencies = [ 348 | { name = "anyio" }, 349 | ] 350 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 351 | wheels = [ 352 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 353 | ] 354 | 355 | [[package]] 356 | name = "tomli" 357 | version = "2.2.1" 358 | source = { registry = "https://pypi.org/simple" } 359 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 362 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 363 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 364 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 365 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 366 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 367 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 368 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 369 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 370 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 371 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 372 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 373 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 374 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 375 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 376 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 377 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 378 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 379 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 380 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 381 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 382 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 383 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 384 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 385 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 386 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 387 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 388 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 389 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 390 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 391 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 392 | ] 393 | 394 | [[package]] 395 | name = "typing-extensions" 396 | version = "4.12.2" 397 | source = { registry = "https://pypi.org/simple" } 398 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 399 | wheels = [ 400 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 401 | ] 402 | 403 | [[package]] 404 | name = "uvicorn" 405 | version = "0.34.0" 406 | source = { registry = "https://pypi.org/simple" } 407 | dependencies = [ 408 | { name = "click" }, 409 | { name = "h11" }, 410 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 411 | ] 412 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 413 | wheels = [ 414 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 415 | ] 416 | --------------------------------------------------------------------------------