├── .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 |
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 |
--------------------------------------------------------------------------------