This is page 4 of 4. Use http://codebase.md/disler/just-prompt?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ ├── commands │ │ ├── context_prime_eza.md │ │ ├── context_prime_w_lead.md │ │ ├── context_prime.md │ │ ├── jprompt_ultra_diff_review.md │ │ ├── project_hello_w_name.md │ │ └── project_hello.md │ └── settings.json ├── .env.sample ├── .gitignore ├── .mcp.json ├── .python-version ├── ai_docs │ ├── extending_thinking_sonny.md │ ├── google-genai-api-update.md │ ├── llm_providers_details.xml │ ├── openai-reasoning-effort.md │ └── pocket-pick-mcp-server-example.xml ├── example_outputs │ ├── countdown_component │ │ ├── countdown_component_groq_qwen-qwq-32b.md │ │ ├── countdown_component_o_gpt-4.5-preview.md │ │ ├── countdown_component_openai_o3-mini.md │ │ ├── countdown_component_q_deepseek-r1-distill-llama-70b-specdec.md │ │ └── diff.md │ └── decision_openai_vs_anthropic_vs_google │ ├── ceo_decision.md │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google_anthropic_claude-3-7-sonnet-20250219_4k.md │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google_gemini_gemini-2.5-flash-preview-04-17.md │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google_gemini_gemini-2.5-pro-preview-03-25.md │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google_openai_o3_high.md │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google_openai_o4-mini_high.md │ └── ceo_prompt.xml ├── images │ ├── just-prompt-logo.png │ └── o3-as-a-ceo.png ├── list_models.py ├── prompts │ ├── ceo_medium_decision_openai_vs_anthropic_vs_google.txt │ ├── ceo_small_decision_python_vs_typescript.txt │ ├── ceo_small_decision_rust_vs_prompt_eng.txt │ ├── countdown_component.txt │ ├── mock_bin_search.txt │ └── mock_ui_component.txt ├── pyproject.toml ├── README.md ├── specs │ ├── gemini-2-5-flash-reasoning.md │ ├── init-just-prompt.md │ ├── new-tool-llm-as-a-ceo.md │ ├── oai-reasoning-levels.md │ └── prompt_from_file_to_file_w_context.md ├── src │ └── just_prompt │ ├── __init__.py │ ├── __main__.py │ ├── atoms │ │ ├── __init__.py │ │ ├── llm_providers │ │ │ ├── __init__.py │ │ │ ├── anthropic.py │ │ │ ├── deepseek.py │ │ │ ├── gemini.py │ │ │ ├── groq.py │ │ │ ├── ollama.py │ │ │ └── openai.py │ │ └── shared │ │ ├── __init__.py │ │ ├── data_types.py │ │ ├── model_router.py │ │ ├── utils.py │ │ └── validator.py │ ├── molecules │ │ ├── __init__.py │ │ ├── ceo_and_board_prompt.py │ │ ├── list_models.py │ │ ├── list_providers.py │ │ ├── prompt_from_file_to_file.py │ │ ├── prompt_from_file.py │ │ └── prompt.py │ ├── server.py │ └── tests │ ├── __init__.py │ ├── atoms │ │ ├── __init__.py │ │ ├── llm_providers │ │ │ ├── __init__.py │ │ │ ├── test_anthropic.py │ │ │ ├── test_deepseek.py │ │ │ ├── test_gemini.py │ │ │ ├── test_groq.py │ │ │ ├── test_ollama.py │ │ │ └── test_openai.py │ │ └── shared │ │ ├── __init__.py │ │ ├── test_model_router.py │ │ ├── test_utils.py │ │ └── test_validator.py │ └── molecules │ ├── __init__.py │ ├── test_ceo_and_board_prompt.py │ ├── test_list_models.py │ ├── test_list_providers.py │ ├── test_prompt_from_file_to_file.py │ ├── test_prompt_from_file.py │ └── test_prompt.py ├── ultra_diff_review │ ├── diff_anthropic_claude-3-7-sonnet-20250219_4k.md │ ├── diff_gemini_gemini-2.0-flash-thinking-exp.md │ ├── diff_openai_o3-mini.md │ └── fusion_ultra_diff_review.md └── uv.lock ``` # Files -------------------------------------------------------------------------------- /ai_docs/pocket-pick-mcp-server-example.xml: -------------------------------------------------------------------------------- ``` 1 | This file is a merged representation of the entire codebase, combined into a single document by Repomix. 2 | 3 | <file_summary> 4 | This section contains a summary of this file. 5 | 6 | <purpose> 7 | This file contains a packed representation of the entire repository's contents. 8 | It is designed to be easily consumable by AI systems for analysis, code review, 9 | or other automated processes. 10 | </purpose> 11 | 12 | <file_format> 13 | The content is organized as follows: 14 | 1. This summary section 15 | 2. Repository information 16 | 3. Directory structure 17 | 4. Repository files, each consisting of: 18 | - File path as an attribute 19 | - Full contents of the file 20 | </file_format> 21 | 22 | <usage_guidelines> 23 | - This file should be treated as read-only. Any changes should be made to the 24 | original repository files, not this packed version. 25 | - When processing this file, use the file path to distinguish 26 | between different files in the repository. 27 | - Be aware that this file may contain sensitive information. Handle it with 28 | the same level of security as you would the original repository. 29 | </usage_guidelines> 30 | 31 | <notes> 32 | - Some files may have been excluded based on .gitignore rules and Repomix's configuration 33 | - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files 34 | - Files matching patterns in .gitignore are excluded 35 | - Files matching default ignore patterns are excluded 36 | - Files are sorted by Git change count (files with more changes are at the bottom) 37 | </notes> 38 | 39 | <additional_info> 40 | 41 | </additional_info> 42 | 43 | </file_summary> 44 | 45 | <directory_structure> 46 | ai_docs/ 47 | mcp-server-git-repomix-output.xml 48 | specs/ 49 | pocket-pick-v1.md 50 | src/ 51 | mcp_server_pocket_pick/ 52 | modules/ 53 | functionality/ 54 | __init__.py 55 | add_file.py 56 | add.py 57 | backup.py 58 | find.py 59 | get.py 60 | list_tags.py 61 | list.py 62 | remove.py 63 | to_file_by_id.py 64 | __init__.py 65 | constants.py 66 | data_types.py 67 | init_db.py 68 | tests/ 69 | functionality/ 70 | __init__.py 71 | test_add_file.py 72 | test_add.py 73 | test_backup.py 74 | test_find.py 75 | test_list_tags.py 76 | test_list.py 77 | test_remove_get.py 78 | test_to_file_by_id.py 79 | __init__.py 80 | test_init_db.py 81 | __init__.py 82 | __main__.py 83 | server.py 84 | .gitignore 85 | .python-version 86 | pyproject.toml 87 | README.md 88 | uv.lock 89 | </directory_structure> 90 | 91 | <files> 92 | This section contains the contents of the repository's files. 93 | 94 | <file path="ai_docs/mcp-server-git-repomix-output.xml"> 95 | This file is a merged representation of the entire codebase, combined into a single document by Repomix. 96 | 97 | <file_summary> 98 | This section contains a summary of this file. 99 | 100 | <purpose> 101 | This file contains a packed representation of the entire repository's contents. 102 | It is designed to be easily consumable by AI systems for analysis, code review, 103 | or other automated processes. 104 | </purpose> 105 | 106 | <file_format> 107 | The content is organized as follows: 108 | 1. This summary section 109 | 2. Repository information 110 | 3. Directory structure 111 | 4. Repository files, each consisting of: 112 | - File path as an attribute 113 | - Full contents of the file 114 | </file_format> 115 | 116 | <usage_guidelines> 117 | - This file should be treated as read-only. Any changes should be made to the 118 | original repository files, not this packed version. 119 | - When processing this file, use the file path to distinguish 120 | between different files in the repository. 121 | - Be aware that this file may contain sensitive information. Handle it with 122 | the same level of security as you would the original repository. 123 | </usage_guidelines> 124 | 125 | <notes> 126 | - Some files may have been excluded based on .gitignore rules and Repomix's configuration 127 | - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files 128 | - Files matching patterns in .gitignore are excluded 129 | - Files matching default ignore patterns are excluded 130 | </notes> 131 | 132 | <additional_info> 133 | 134 | </additional_info> 135 | 136 | </file_summary> 137 | 138 | <directory_structure> 139 | src/ 140 | mcp_server_git/ 141 | __init__.py 142 | __main__.py 143 | server.py 144 | tests/ 145 | test_server.py 146 | .gitignore 147 | .python-version 148 | Dockerfile 149 | LICENSE 150 | pyproject.toml 151 | README.md 152 | uv.lock 153 | </directory_structure> 154 | 155 | <files> 156 | This section contains the contents of the repository's files. 157 | 158 | <file path="src/mcp_server_git/__init__.py"> 159 | import click 160 | from pathlib import Path 161 | import logging 162 | import sys 163 | from .server import serve 164 | 165 | @click.command() 166 | @click.option("--repository", "-r", type=Path, help="Git repository path") 167 | @click.option("-v", "--verbose", count=True) 168 | def main(repository: Path | None, verbose: bool) -> None: 169 | """MCP Git Server - Git functionality for MCP""" 170 | import asyncio 171 | 172 | logging_level = logging.WARN 173 | if verbose == 1: 174 | logging_level = logging.INFO 175 | elif verbose >= 2: 176 | logging_level = logging.DEBUG 177 | 178 | logging.basicConfig(level=logging_level, stream=sys.stderr) 179 | asyncio.run(serve(repository)) 180 | 181 | if __name__ == "__main__": 182 | main() 183 | </file> 184 | 185 | <file path="src/mcp_server_git/__main__.py"> 186 | # __main__.py 187 | 188 | from mcp_server_git import main 189 | 190 | main() 191 | </file> 192 | 193 | <file path="src/mcp_server_git/server.py"> 194 | import logging 195 | from pathlib import Path 196 | from typing import Sequence 197 | from mcp.server import Server 198 | from mcp.server.session import ServerSession 199 | from mcp.server.stdio import stdio_server 200 | from mcp.types import ( 201 | ClientCapabilities, 202 | TextContent, 203 | Tool, 204 | ListRootsResult, 205 | RootsCapability, 206 | ) 207 | from enum import Enum 208 | import git 209 | from pydantic import BaseModel 210 | 211 | class GitStatus(BaseModel): 212 | repo_path: str 213 | 214 | class GitDiffUnstaged(BaseModel): 215 | repo_path: str 216 | 217 | class GitDiffStaged(BaseModel): 218 | repo_path: str 219 | 220 | class GitDiff(BaseModel): 221 | repo_path: str 222 | target: str 223 | 224 | class GitCommit(BaseModel): 225 | repo_path: str 226 | message: str 227 | 228 | class GitAdd(BaseModel): 229 | repo_path: str 230 | files: list[str] 231 | 232 | class GitReset(BaseModel): 233 | repo_path: str 234 | 235 | class GitLog(BaseModel): 236 | repo_path: str 237 | max_count: int = 10 238 | 239 | class GitCreateBranch(BaseModel): 240 | repo_path: str 241 | branch_name: str 242 | base_branch: str | None = None 243 | 244 | class GitCheckout(BaseModel): 245 | repo_path: str 246 | branch_name: str 247 | 248 | class GitShow(BaseModel): 249 | repo_path: str 250 | revision: str 251 | 252 | class GitInit(BaseModel): 253 | repo_path: str 254 | 255 | class GitTools(str, Enum): 256 | STATUS = "git_status" 257 | DIFF_UNSTAGED = "git_diff_unstaged" 258 | DIFF_STAGED = "git_diff_staged" 259 | DIFF = "git_diff" 260 | COMMIT = "git_commit" 261 | ADD = "git_add" 262 | RESET = "git_reset" 263 | LOG = "git_log" 264 | CREATE_BRANCH = "git_create_branch" 265 | CHECKOUT = "git_checkout" 266 | SHOW = "git_show" 267 | INIT = "git_init" 268 | 269 | def git_status(repo: git.Repo) -> str: 270 | return repo.git.status() 271 | 272 | def git_diff_unstaged(repo: git.Repo) -> str: 273 | return repo.git.diff() 274 | 275 | def git_diff_staged(repo: git.Repo) -> str: 276 | return repo.git.diff("--cached") 277 | 278 | def git_diff(repo: git.Repo, target: str) -> str: 279 | return repo.git.diff(target) 280 | 281 | def git_commit(repo: git.Repo, message: str) -> str: 282 | commit = repo.index.commit(message) 283 | return f"Changes committed successfully with hash {commit.hexsha}" 284 | 285 | def git_add(repo: git.Repo, files: list[str]) -> str: 286 | repo.index.add(files) 287 | return "Files staged successfully" 288 | 289 | def git_reset(repo: git.Repo) -> str: 290 | repo.index.reset() 291 | return "All staged changes reset" 292 | 293 | def git_log(repo: git.Repo, max_count: int = 10) -> list[str]: 294 | commits = list(repo.iter_commits(max_count=max_count)) 295 | log = [] 296 | for commit in commits: 297 | log.append( 298 | f"Commit: {commit.hexsha}\n" 299 | f"Author: {commit.author}\n" 300 | f"Date: {commit.authored_datetime}\n" 301 | f"Message: {commit.message}\n" 302 | ) 303 | return log 304 | 305 | def git_create_branch(repo: git.Repo, branch_name: str, base_branch: str | None = None) -> str: 306 | if base_branch: 307 | base = repo.refs[base_branch] 308 | else: 309 | base = repo.active_branch 310 | 311 | repo.create_head(branch_name, base) 312 | return f"Created branch '{branch_name}' from '{base.name}'" 313 | 314 | def git_checkout(repo: git.Repo, branch_name: str) -> str: 315 | repo.git.checkout(branch_name) 316 | return f"Switched to branch '{branch_name}'" 317 | 318 | def git_init(repo_path: str) -> str: 319 | try: 320 | repo = git.Repo.init(path=repo_path, mkdir=True) 321 | return f"Initialized empty Git repository in {repo.git_dir}" 322 | except Exception as e: 323 | return f"Error initializing repository: {str(e)}" 324 | 325 | def git_show(repo: git.Repo, revision: str) -> str: 326 | commit = repo.commit(revision) 327 | output = [ 328 | f"Commit: {commit.hexsha}\n" 329 | f"Author: {commit.author}\n" 330 | f"Date: {commit.authored_datetime}\n" 331 | f"Message: {commit.message}\n" 332 | ] 333 | if commit.parents: 334 | parent = commit.parents[0] 335 | diff = parent.diff(commit, create_patch=True) 336 | else: 337 | diff = commit.diff(git.NULL_TREE, create_patch=True) 338 | for d in diff: 339 | output.append(f"\n--- {d.a_path}\n+++ {d.b_path}\n") 340 | output.append(d.diff.decode('utf-8')) 341 | return "".join(output) 342 | 343 | async def serve(repository: Path | None) -> None: 344 | logger = logging.getLogger(__name__) 345 | 346 | if repository is not None: 347 | try: 348 | git.Repo(repository) 349 | logger.info(f"Using repository at {repository}") 350 | except git.InvalidGitRepositoryError: 351 | logger.error(f"{repository} is not a valid Git repository") 352 | return 353 | 354 | server = Server("mcp-git") 355 | 356 | @server.list_tools() 357 | async def list_tools() -> list[Tool]: 358 | return [ 359 | Tool( 360 | name=GitTools.STATUS, 361 | description="Shows the working tree status", 362 | inputSchema=GitStatus.schema(), 363 | ), 364 | Tool( 365 | name=GitTools.DIFF_UNSTAGED, 366 | description="Shows changes in the working directory that are not yet staged", 367 | inputSchema=GitDiffUnstaged.schema(), 368 | ), 369 | Tool( 370 | name=GitTools.DIFF_STAGED, 371 | description="Shows changes that are staged for commit", 372 | inputSchema=GitDiffStaged.schema(), 373 | ), 374 | Tool( 375 | name=GitTools.DIFF, 376 | description="Shows differences between branches or commits", 377 | inputSchema=GitDiff.schema(), 378 | ), 379 | Tool( 380 | name=GitTools.COMMIT, 381 | description="Records changes to the repository", 382 | inputSchema=GitCommit.schema(), 383 | ), 384 | Tool( 385 | name=GitTools.ADD, 386 | description="Adds file contents to the staging area", 387 | inputSchema=GitAdd.schema(), 388 | ), 389 | Tool( 390 | name=GitTools.RESET, 391 | description="Unstages all staged changes", 392 | inputSchema=GitReset.schema(), 393 | ), 394 | Tool( 395 | name=GitTools.LOG, 396 | description="Shows the commit logs", 397 | inputSchema=GitLog.schema(), 398 | ), 399 | Tool( 400 | name=GitTools.CREATE_BRANCH, 401 | description="Creates a new branch from an optional base branch", 402 | inputSchema=GitCreateBranch.schema(), 403 | ), 404 | Tool( 405 | name=GitTools.CHECKOUT, 406 | description="Switches branches", 407 | inputSchema=GitCheckout.schema(), 408 | ), 409 | Tool( 410 | name=GitTools.SHOW, 411 | description="Shows the contents of a commit", 412 | inputSchema=GitShow.schema(), 413 | ), 414 | Tool( 415 | name=GitTools.INIT, 416 | description="Initialize a new Git repository", 417 | inputSchema=GitInit.schema(), 418 | ) 419 | ] 420 | 421 | async def list_repos() -> Sequence[str]: 422 | async def by_roots() -> Sequence[str]: 423 | if not isinstance(server.request_context.session, ServerSession): 424 | raise TypeError("server.request_context.session must be a ServerSession") 425 | 426 | if not server.request_context.session.check_client_capability( 427 | ClientCapabilities(roots=RootsCapability()) 428 | ): 429 | return [] 430 | 431 | roots_result: ListRootsResult = await server.request_context.session.list_roots() 432 | logger.debug(f"Roots result: {roots_result}") 433 | repo_paths = [] 434 | for root in roots_result.roots: 435 | path = root.uri.path 436 | try: 437 | git.Repo(path) 438 | repo_paths.append(str(path)) 439 | except git.InvalidGitRepositoryError: 440 | pass 441 | return repo_paths 442 | 443 | def by_commandline() -> Sequence[str]: 444 | return [str(repository)] if repository is not None else [] 445 | 446 | cmd_repos = by_commandline() 447 | root_repos = await by_roots() 448 | return [*root_repos, *cmd_repos] 449 | 450 | @server.call_tool() 451 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 452 | repo_path = Path(arguments["repo_path"]) 453 | 454 | # Handle git init separately since it doesn't require an existing repo 455 | if name == GitTools.INIT: 456 | result = git_init(str(repo_path)) 457 | return [TextContent( 458 | type="text", 459 | text=result 460 | )] 461 | 462 | # For all other commands, we need an existing repo 463 | repo = git.Repo(repo_path) 464 | 465 | match name: 466 | case GitTools.STATUS: 467 | status = git_status(repo) 468 | return [TextContent( 469 | type="text", 470 | text=f"Repository status:\n{status}" 471 | )] 472 | 473 | case GitTools.DIFF_UNSTAGED: 474 | diff = git_diff_unstaged(repo) 475 | return [TextContent( 476 | type="text", 477 | text=f"Unstaged changes:\n{diff}" 478 | )] 479 | 480 | case GitTools.DIFF_STAGED: 481 | diff = git_diff_staged(repo) 482 | return [TextContent( 483 | type="text", 484 | text=f"Staged changes:\n{diff}" 485 | )] 486 | 487 | case GitTools.DIFF: 488 | diff = git_diff(repo, arguments["target"]) 489 | return [TextContent( 490 | type="text", 491 | text=f"Diff with {arguments['target']}:\n{diff}" 492 | )] 493 | 494 | case GitTools.COMMIT: 495 | result = git_commit(repo, arguments["message"]) 496 | return [TextContent( 497 | type="text", 498 | text=result 499 | )] 500 | 501 | case GitTools.ADD: 502 | result = git_add(repo, arguments["files"]) 503 | return [TextContent( 504 | type="text", 505 | text=result 506 | )] 507 | 508 | case GitTools.RESET: 509 | result = git_reset(repo) 510 | return [TextContent( 511 | type="text", 512 | text=result 513 | )] 514 | 515 | case GitTools.LOG: 516 | log = git_log(repo, arguments.get("max_count", 10)) 517 | return [TextContent( 518 | type="text", 519 | text="Commit history:\n" + "\n".join(log) 520 | )] 521 | 522 | case GitTools.CREATE_BRANCH: 523 | result = git_create_branch( 524 | repo, 525 | arguments["branch_name"], 526 | arguments.get("base_branch") 527 | ) 528 | return [TextContent( 529 | type="text", 530 | text=result 531 | )] 532 | 533 | case GitTools.CHECKOUT: 534 | result = git_checkout(repo, arguments["branch_name"]) 535 | return [TextContent( 536 | type="text", 537 | text=result 538 | )] 539 | 540 | case GitTools.SHOW: 541 | result = git_show(repo, arguments["revision"]) 542 | return [TextContent( 543 | type="text", 544 | text=result 545 | )] 546 | 547 | case _: 548 | raise ValueError(f"Unknown tool: {name}") 549 | 550 | options = server.create_initialization_options() 551 | async with stdio_server() as (read_stream, write_stream): 552 | await server.run(read_stream, write_stream, options, raise_exceptions=True) 553 | </file> 554 | 555 | <file path="tests/test_server.py"> 556 | import pytest 557 | from pathlib import Path 558 | import git 559 | from mcp_server_git.server import git_checkout 560 | import shutil 561 | 562 | @pytest.fixture 563 | def test_repository(tmp_path: Path): 564 | repo_path = tmp_path / "temp_test_repo" 565 | test_repo = git.Repo.init(repo_path) 566 | 567 | Path(repo_path / "test.txt").write_text("test") 568 | test_repo.index.add(["test.txt"]) 569 | test_repo.index.commit("initial commit") 570 | 571 | yield test_repo 572 | 573 | shutil.rmtree(repo_path) 574 | 575 | def test_git_checkout_existing_branch(test_repository): 576 | test_repository.git.branch("test-branch") 577 | result = git_checkout(test_repository, "test-branch") 578 | 579 | assert "Switched to branch 'test-branch'" in result 580 | assert test_repository.active_branch.name == "test-branch" 581 | 582 | def test_git_checkout_nonexistent_branch(test_repository): 583 | 584 | with pytest.raises(git.GitCommandError): 585 | git_checkout(test_repository, "nonexistent-branch") 586 | </file> 587 | 588 | <file path=".gitignore"> 589 | __pycache__ 590 | .venv 591 | </file> 592 | 593 | <file path=".python-version"> 594 | 3.10 595 | </file> 596 | 597 | <file path="Dockerfile"> 598 | # Use a Python image with uv pre-installed 599 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv 600 | 601 | # Install the project into `/app` 602 | WORKDIR /app 603 | 604 | # Enable bytecode compilation 605 | ENV UV_COMPILE_BYTECODE=1 606 | 607 | # Copy from the cache instead of linking since it's a mounted volume 608 | ENV UV_LINK_MODE=copy 609 | 610 | # Install the project's dependencies using the lockfile and settings 611 | RUN --mount=type=cache,target=/root/.cache/uv \ 612 | --mount=type=bind,source=uv.lock,target=uv.lock \ 613 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 614 | uv sync --frozen --no-install-project --no-dev --no-editable 615 | 616 | # Then, add the rest of the project source code and install it 617 | # Installing separately from its dependencies allows optimal layer caching 618 | ADD . /app 619 | RUN --mount=type=cache,target=/root/.cache/uv \ 620 | uv sync --frozen --no-dev --no-editable 621 | 622 | FROM python:3.12-slim-bookworm 623 | 624 | RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* 625 | 626 | WORKDIR /app 627 | 628 | COPY --from=uv /root/.local /root/.local 629 | COPY --from=uv --chown=app:app /app/.venv /app/.venv 630 | 631 | # Place executables in the environment at the front of the path 632 | ENV PATH="/app/.venv/bin:$PATH" 633 | 634 | # when running the container, add --db-path and a bind mount to the host's db file 635 | ENTRYPOINT ["mcp-server-git"] 636 | </file> 637 | 638 | <file path="LICENSE"> 639 | Copyright (c) 2024 Anthropic, PBC. 640 | 641 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 642 | 643 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 644 | 645 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 646 | </file> 647 | 648 | <file path="pyproject.toml"> 649 | [project] 650 | name = "mcp-server-git" 651 | version = "0.6.2" 652 | description = "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs" 653 | readme = "README.md" 654 | requires-python = ">=3.10" 655 | authors = [{ name = "Anthropic, PBC." }] 656 | maintainers = [{ name = "David Soria Parra", email = "[email protected]" }] 657 | keywords = ["git", "mcp", "llm", "automation"] 658 | license = { text = "MIT" } 659 | classifiers = [ 660 | "Development Status :: 4 - Beta", 661 | "Intended Audience :: Developers", 662 | "License :: OSI Approved :: MIT License", 663 | "Programming Language :: Python :: 3", 664 | "Programming Language :: Python :: 3.10", 665 | ] 666 | dependencies = [ 667 | "click>=8.1.7", 668 | "gitpython>=3.1.43", 669 | "mcp>=1.0.0", 670 | "pydantic>=2.0.0", 671 | ] 672 | 673 | [project.scripts] 674 | mcp-server-git = "mcp_server_git:main" 675 | 676 | [build-system] 677 | requires = ["hatchling"] 678 | build-backend = "hatchling.build" 679 | 680 | [tool.uv] 681 | dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3", "pytest>=8.0.0"] 682 | 683 | [tool.pytest.ini_options] 684 | testpaths = ["tests"] 685 | python_files = "test_*.py" 686 | python_classes = "Test*" 687 | python_functions = "test_*" 688 | </file> 689 | 690 | <file path="README.md"> 691 | # mcp-server-git: A git MCP server 692 | 693 | ## Overview 694 | 695 | A Model Context Protocol server for Git repository interaction and automation. This server provides tools to read, search, and manipulate Git repositories via Large Language Models. 696 | 697 | Please note that mcp-server-git is currently in early development. The functionality and available tools are subject to change and expansion as we continue to develop and improve the server. 698 | 699 | ### Tools 700 | 701 | 1. `git_status` 702 | - Shows the working tree status 703 | - Input: 704 | - `repo_path` (string): Path to Git repository 705 | - Returns: Current status of working directory as text output 706 | 707 | 2. `git_diff_unstaged` 708 | - Shows changes in working directory not yet staged 709 | - Input: 710 | - `repo_path` (string): Path to Git repository 711 | - Returns: Diff output of unstaged changes 712 | 713 | 3. `git_diff_staged` 714 | - Shows changes that are staged for commit 715 | - Input: 716 | - `repo_path` (string): Path to Git repository 717 | - Returns: Diff output of staged changes 718 | 719 | 4. `git_diff` 720 | - Shows differences between branches or commits 721 | - Inputs: 722 | - `repo_path` (string): Path to Git repository 723 | - `target` (string): Target branch or commit to compare with 724 | - Returns: Diff output comparing current state with target 725 | 726 | 5. `git_commit` 727 | - Records changes to the repository 728 | - Inputs: 729 | - `repo_path` (string): Path to Git repository 730 | - `message` (string): Commit message 731 | - Returns: Confirmation with new commit hash 732 | 733 | 6. `git_add` 734 | - Adds file contents to the staging area 735 | - Inputs: 736 | - `repo_path` (string): Path to Git repository 737 | - `files` (string[]): Array of file paths to stage 738 | - Returns: Confirmation of staged files 739 | 740 | 7. `git_reset` 741 | - Unstages all staged changes 742 | - Input: 743 | - `repo_path` (string): Path to Git repository 744 | - Returns: Confirmation of reset operation 745 | 746 | 8. `git_log` 747 | - Shows the commit logs 748 | - Inputs: 749 | - `repo_path` (string): Path to Git repository 750 | - `max_count` (number, optional): Maximum number of commits to show (default: 10) 751 | - Returns: Array of commit entries with hash, author, date, and message 752 | 753 | 9. `git_create_branch` 754 | - Creates a new branch 755 | - Inputs: 756 | - `repo_path` (string): Path to Git repository 757 | - `branch_name` (string): Name of the new branch 758 | - `start_point` (string, optional): Starting point for the new branch 759 | - Returns: Confirmation of branch creation 760 | 10. `git_checkout` 761 | - Switches branches 762 | - Inputs: 763 | - `repo_path` (string): Path to Git repository 764 | - `branch_name` (string): Name of branch to checkout 765 | - Returns: Confirmation of branch switch 766 | 11. `git_show` 767 | - Shows the contents of a commit 768 | - Inputs: 769 | - `repo_path` (string): Path to Git repository 770 | - `revision` (string): The revision (commit hash, branch name, tag) to show 771 | - Returns: Contents of the specified commit 772 | 12. `git_init` 773 | - Initializes a Git repository 774 | - Inputs: 775 | - `repo_path` (string): Path to directory to initialize git repo 776 | - Returns: Confirmation of repository initialization 777 | 778 | ## Installation 779 | 780 | ### Using uv (recommended) 781 | 782 | When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will 783 | use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-git*. 784 | 785 | ### Using PIP 786 | 787 | Alternatively you can install `mcp-server-git` via pip: 788 | 789 | ``` 790 | pip install mcp-server-git 791 | ``` 792 | 793 | After installation, you can run it as a script using: 794 | 795 | ``` 796 | python -m mcp_server_git 797 | ``` 798 | 799 | ## Configuration 800 | 801 | ### Usage with Claude Desktop 802 | 803 | Add this to your `claude_desktop_config.json`: 804 | 805 | <details> 806 | <summary>Using uvx</summary> 807 | 808 | ```json 809 | "mcpServers": { 810 | "git": { 811 | "command": "uvx", 812 | "args": ["mcp-server-git", "--repository", "path/to/git/repo"] 813 | } 814 | } 815 | ``` 816 | </details> 817 | 818 | <details> 819 | <summary>Using docker</summary> 820 | 821 | * Note: replace '/Users/username' with the a path that you want to be accessible by this tool 822 | 823 | ```json 824 | "mcpServers": { 825 | "git": { 826 | "command": "docker", 827 | "args": ["run", "--rm", "-i", "--mount", "type=bind,src=/Users/username,dst=/Users/username", "mcp/git"] 828 | } 829 | } 830 | ``` 831 | </details> 832 | 833 | <details> 834 | <summary>Using pip installation</summary> 835 | 836 | ```json 837 | "mcpServers": { 838 | "git": { 839 | "command": "python", 840 | "args": ["-m", "mcp_server_git", "--repository", "path/to/git/repo"] 841 | } 842 | } 843 | ``` 844 | </details> 845 | 846 | ### Usage with [Zed](https://github.com/zed-industries/zed) 847 | 848 | Add to your Zed settings.json: 849 | 850 | <details> 851 | <summary>Using uvx</summary> 852 | 853 | ```json 854 | "context_servers": [ 855 | "mcp-server-git": { 856 | "command": { 857 | "path": "uvx", 858 | "args": ["mcp-server-git"] 859 | } 860 | } 861 | ], 862 | ``` 863 | </details> 864 | 865 | <details> 866 | <summary>Using pip installation</summary> 867 | 868 | ```json 869 | "context_servers": { 870 | "mcp-server-git": { 871 | "command": { 872 | "path": "python", 873 | "args": ["-m", "mcp_server_git"] 874 | } 875 | } 876 | }, 877 | ``` 878 | </details> 879 | 880 | ## Debugging 881 | 882 | You can use the MCP inspector to debug the server. For uvx installations: 883 | 884 | ``` 885 | npx @modelcontextprotocol/inspector uvx mcp-server-git 886 | ``` 887 | 888 | Or if you've installed the package in a specific directory or are developing on it: 889 | 890 | ``` 891 | cd path/to/servers/src/git 892 | npx @modelcontextprotocol/inspector uv run mcp-server-git 893 | ``` 894 | 895 | Running `tail -n 20 -f ~/Library/Logs/Claude/mcp*.log` will show the logs from the server and may 896 | help you debug any issues. 897 | 898 | ## Development 899 | 900 | If you are doing local development, there are two ways to test your changes: 901 | 902 | 1. Run the MCP inspector to test your changes. See [Debugging](#debugging) for run instructions. 903 | 904 | 2. Test using the Claude desktop app. Add the following to your `claude_desktop_config.json`: 905 | 906 | ### Docker 907 | 908 | ```json 909 | { 910 | "mcpServers": { 911 | "git": { 912 | "command": "docker", 913 | "args": [ 914 | "run", 915 | "--rm", 916 | "-i", 917 | "--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop", 918 | "--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro", 919 | "--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt", 920 | "mcp/git" 921 | ] 922 | } 923 | } 924 | } 925 | ``` 926 | 927 | ### UVX 928 | ```json 929 | { 930 | "mcpServers": { 931 | "git": { 932 | "command": "uv", 933 | "args": [ 934 | "--directory", 935 | "/<path to mcp-servers>/mcp-servers/src/git", 936 | "run", 937 | "mcp-server-git" 938 | ] 939 | } 940 | } 941 | ``` 942 | 943 | ## Build 944 | 945 | Docker build: 946 | 947 | ```bash 948 | cd src/git 949 | docker build -t mcp/git . 950 | ``` 951 | 952 | ## License 953 | 954 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. 955 | </file> 956 | 957 | <file path="uv.lock"> 958 | version = 1 959 | requires-python = ">=3.10" 960 | 961 | [[package]] 962 | name = "annotated-types" 963 | version = "0.7.0" 964 | source = { registry = "https://pypi.org/simple" } 965 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 966 | wheels = [ 967 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 968 | ] 969 | 970 | [[package]] 971 | name = "anyio" 972 | version = "4.6.2.post1" 973 | source = { registry = "https://pypi.org/simple" } 974 | dependencies = [ 975 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 976 | { name = "idna" }, 977 | { name = "sniffio" }, 978 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 979 | ] 980 | sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } 981 | wheels = [ 982 | { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, 983 | ] 984 | 985 | [[package]] 986 | name = "certifi" 987 | version = "2024.8.30" 988 | source = { registry = "https://pypi.org/simple" } 989 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } 990 | wheels = [ 991 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, 992 | ] 993 | 994 | [[package]] 995 | name = "click" 996 | version = "8.1.7" 997 | source = { registry = "https://pypi.org/simple" } 998 | dependencies = [ 999 | { name = "colorama", marker = "platform_system == 'Windows'" }, 1000 | ] 1001 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } 1002 | wheels = [ 1003 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "colorama" 1008 | version = "0.4.6" 1009 | source = { registry = "https://pypi.org/simple" } 1010 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 1011 | wheels = [ 1012 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "exceptiongroup" 1017 | version = "1.2.2" 1018 | source = { registry = "https://pypi.org/simple" } 1019 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 1020 | wheels = [ 1021 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "gitdb" 1026 | version = "4.0.11" 1027 | source = { registry = "https://pypi.org/simple" } 1028 | dependencies = [ 1029 | { name = "smmap" }, 1030 | ] 1031 | sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } 1032 | wheels = [ 1033 | { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "gitpython" 1038 | version = "3.1.43" 1039 | source = { registry = "https://pypi.org/simple" } 1040 | dependencies = [ 1041 | { name = "gitdb" }, 1042 | ] 1043 | sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } 1044 | wheels = [ 1045 | { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "h11" 1050 | version = "0.14.0" 1051 | source = { registry = "https://pypi.org/simple" } 1052 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 1053 | wheels = [ 1054 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "httpcore" 1059 | version = "1.0.7" 1060 | source = { registry = "https://pypi.org/simple" } 1061 | dependencies = [ 1062 | { name = "certifi" }, 1063 | { name = "h11" }, 1064 | ] 1065 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 1066 | wheels = [ 1067 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "httpx" 1072 | version = "0.27.2" 1073 | source = { registry = "https://pypi.org/simple" } 1074 | dependencies = [ 1075 | { name = "anyio" }, 1076 | { name = "certifi" }, 1077 | { name = "httpcore" }, 1078 | { name = "idna" }, 1079 | { name = "sniffio" }, 1080 | ] 1081 | sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } 1082 | wheels = [ 1083 | { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "httpx-sse" 1088 | version = "0.4.0" 1089 | source = { registry = "https://pypi.org/simple" } 1090 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 1091 | wheels = [ 1092 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "idna" 1097 | version = "3.10" 1098 | source = { registry = "https://pypi.org/simple" } 1099 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 1100 | wheels = [ 1101 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "iniconfig" 1106 | version = "2.0.0" 1107 | source = { registry = "https://pypi.org/simple" } 1108 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 1109 | wheels = [ 1110 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "mcp" 1115 | version = "1.1.0" 1116 | source = { registry = "https://pypi.org/simple" } 1117 | dependencies = [ 1118 | { name = "anyio" }, 1119 | { name = "httpx" }, 1120 | { name = "httpx-sse" }, 1121 | { name = "pydantic" }, 1122 | { name = "sse-starlette" }, 1123 | { name = "starlette" }, 1124 | ] 1125 | sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 } 1126 | wheels = [ 1127 | { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 }, 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "mcp-server-git" 1132 | version = "0.6.2" 1133 | source = { editable = "." } 1134 | dependencies = [ 1135 | { name = "click" }, 1136 | { name = "gitpython" }, 1137 | { name = "mcp" }, 1138 | { name = "pydantic" }, 1139 | ] 1140 | 1141 | [package.dev-dependencies] 1142 | dev = [ 1143 | { name = "pyright" }, 1144 | { name = "pytest" }, 1145 | { name = "ruff" }, 1146 | ] 1147 | 1148 | [package.metadata] 1149 | requires-dist = [ 1150 | { name = "click", specifier = ">=8.1.7" }, 1151 | { name = "gitpython", specifier = ">=3.1.43" }, 1152 | { name = "mcp", specifier = ">=1.0.0" }, 1153 | { name = "pydantic", specifier = ">=2.0.0" }, 1154 | ] 1155 | 1156 | [package.metadata.requires-dev] 1157 | dev = [ 1158 | { name = "pyright", specifier = ">=1.1.389" }, 1159 | { name = "pytest", specifier = ">=8.0.0" }, 1160 | { name = "ruff", specifier = ">=0.7.3" }, 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "nodeenv" 1165 | version = "1.9.1" 1166 | source = { registry = "https://pypi.org/simple" } 1167 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } 1168 | wheels = [ 1169 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "packaging" 1174 | version = "24.2" 1175 | source = { registry = "https://pypi.org/simple" } 1176 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 1177 | wheels = [ 1178 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "pluggy" 1183 | version = "1.5.0" 1184 | source = { registry = "https://pypi.org/simple" } 1185 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 1186 | wheels = [ 1187 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "pydantic" 1192 | version = "2.10.1" 1193 | source = { registry = "https://pypi.org/simple" } 1194 | dependencies = [ 1195 | { name = "annotated-types" }, 1196 | { name = "pydantic-core" }, 1197 | { name = "typing-extensions" }, 1198 | ] 1199 | sdist = { url = "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", size = 783717 } 1200 | wheels = [ 1201 | { url = "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e", size = 455329 }, 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "pydantic-core" 1206 | version = "2.27.1" 1207 | source = { registry = "https://pypi.org/simple" } 1208 | dependencies = [ 1209 | { name = "typing-extensions" }, 1210 | ] 1211 | sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } 1212 | wheels = [ 1213 | { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, 1214 | { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, 1215 | { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, 1216 | { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, 1217 | { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, 1218 | { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, 1219 | { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, 1220 | { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, 1221 | { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, 1222 | { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, 1223 | { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, 1224 | { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, 1225 | { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, 1226 | { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, 1227 | { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, 1228 | { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, 1229 | { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, 1230 | { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, 1231 | { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, 1232 | { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, 1233 | { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, 1234 | { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, 1235 | { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, 1236 | { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, 1237 | { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, 1238 | { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, 1239 | { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, 1240 | { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, 1241 | { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, 1242 | { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, 1243 | { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, 1244 | { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, 1245 | { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, 1246 | { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, 1247 | { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, 1248 | { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, 1249 | { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, 1250 | { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, 1251 | { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, 1252 | { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, 1253 | { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, 1254 | { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, 1255 | { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, 1256 | { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, 1257 | { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, 1258 | { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, 1259 | { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, 1260 | { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, 1261 | { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, 1262 | { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, 1263 | { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, 1264 | { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, 1265 | { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, 1266 | { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, 1267 | { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, 1268 | { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, 1269 | { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, 1270 | { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, 1271 | { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, 1272 | { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, 1273 | { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, 1274 | { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, 1275 | { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, 1276 | { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "pyright" 1281 | version = "1.1.389" 1282 | source = { registry = "https://pypi.org/simple" } 1283 | dependencies = [ 1284 | { name = "nodeenv" }, 1285 | { name = "typing-extensions" }, 1286 | ] 1287 | sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 } 1288 | wheels = [ 1289 | { url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 }, 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "pytest" 1294 | version = "8.3.4" 1295 | source = { registry = "https://pypi.org/simple" } 1296 | dependencies = [ 1297 | { name = "colorama", marker = "sys_platform == 'win32'" }, 1298 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 1299 | { name = "iniconfig" }, 1300 | { name = "packaging" }, 1301 | { name = "pluggy" }, 1302 | { name = "tomli", marker = "python_full_version < '3.11'" }, 1303 | ] 1304 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } 1305 | wheels = [ 1306 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "ruff" 1311 | version = "0.8.0" 1312 | source = { registry = "https://pypi.org/simple" } 1313 | sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } 1314 | wheels = [ 1315 | { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, 1316 | { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, 1317 | { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, 1318 | { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, 1319 | { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, 1320 | { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, 1321 | { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, 1322 | { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, 1323 | { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, 1324 | { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, 1325 | { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, 1326 | { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, 1327 | { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, 1328 | { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, 1329 | { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, 1330 | { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, 1331 | { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, 1332 | ] 1333 | 1334 | [[package]] 1335 | name = "smmap" 1336 | version = "5.0.1" 1337 | source = { registry = "https://pypi.org/simple" } 1338 | sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } 1339 | wheels = [ 1340 | { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "sniffio" 1345 | version = "1.3.1" 1346 | source = { registry = "https://pypi.org/simple" } 1347 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 1348 | wheels = [ 1349 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "sse-starlette" 1354 | version = "2.1.3" 1355 | source = { registry = "https://pypi.org/simple" } 1356 | dependencies = [ 1357 | { name = "anyio" }, 1358 | { name = "starlette" }, 1359 | { name = "uvicorn" }, 1360 | ] 1361 | sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 } 1362 | wheels = [ 1363 | { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 }, 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "starlette" 1368 | version = "0.41.3" 1369 | source = { registry = "https://pypi.org/simple" } 1370 | dependencies = [ 1371 | { name = "anyio" }, 1372 | ] 1373 | sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } 1374 | wheels = [ 1375 | { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "tomli" 1380 | version = "2.2.1" 1381 | source = { registry = "https://pypi.org/simple" } 1382 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 1383 | wheels = [ 1384 | { 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 }, 1385 | { 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 }, 1386 | { 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 }, 1387 | { 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 }, 1388 | { 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 }, 1389 | { 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 }, 1390 | { 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 }, 1391 | { 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 }, 1392 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 1393 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 1394 | { 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 }, 1395 | { 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 }, 1396 | { 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 }, 1397 | { 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 }, 1398 | { 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 }, 1399 | { 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 }, 1400 | { 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 }, 1401 | { 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 }, 1402 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 1403 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 1404 | { 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 }, 1405 | { 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 }, 1406 | { 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 }, 1407 | { 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 }, 1408 | { 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 }, 1409 | { 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 }, 1410 | { 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 }, 1411 | { 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 }, 1412 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 1413 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 1414 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 1415 | ] 1416 | 1417 | [[package]] 1418 | name = "typing-extensions" 1419 | version = "4.12.2" 1420 | source = { registry = "https://pypi.org/simple" } 1421 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 1422 | wheels = [ 1423 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "uvicorn" 1428 | version = "0.32.1" 1429 | source = { registry = "https://pypi.org/simple" } 1430 | dependencies = [ 1431 | { name = "click" }, 1432 | { name = "h11" }, 1433 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 1434 | ] 1435 | sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } 1436 | wheels = [ 1437 | { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, 1438 | ] 1439 | </file> 1440 | 1441 | </files> 1442 | </file> 1443 | 1444 | <file path="src/mcp_server_pocket_pick/modules/functionality/__init__.py"> 1445 | # Functionality module initialization 1446 | </file> 1447 | 1448 | <file path="src/mcp_server_pocket_pick/modules/functionality/add_file.py"> 1449 | import sqlite3 1450 | import uuid 1451 | import json 1452 | from datetime import datetime 1453 | from pathlib import Path 1454 | import logging 1455 | from ..data_types import AddFileCommand, PocketItem 1456 | from ..init_db import init_db, normalize_tags 1457 | 1458 | logger = logging.getLogger(__name__) 1459 | 1460 | def add_file(command: AddFileCommand) -> PocketItem: 1461 | """ 1462 | Add a new item to the pocket pick database from a file 1463 | 1464 | Args: 1465 | command: AddFileCommand with file_path, tags and db_path 1466 | 1467 | Returns: 1468 | PocketItem: The newly created item 1469 | """ 1470 | # Read the file content 1471 | try: 1472 | file_path = Path(command.file_path) 1473 | if not file_path.exists(): 1474 | raise FileNotFoundError(f"File not found: {file_path}") 1475 | 1476 | with open(file_path, 'r', encoding='utf-8') as f: 1477 | text = f.read() 1478 | except Exception as e: 1479 | logger.error(f"Error reading file {command.file_path}: {e}") 1480 | raise 1481 | 1482 | # Normalize tags 1483 | normalized_tags = normalize_tags(command.tags) 1484 | 1485 | # Generate a unique ID 1486 | item_id = str(uuid.uuid4()) 1487 | 1488 | # Get current timestamp 1489 | timestamp = datetime.now() 1490 | 1491 | # Connect to database 1492 | db = init_db(command.db_path) 1493 | 1494 | try: 1495 | # Serialize tags to JSON 1496 | tags_json = json.dumps(normalized_tags) 1497 | 1498 | # Insert item 1499 | db.execute( 1500 | "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", 1501 | (item_id, timestamp.isoformat(), text, tags_json) 1502 | ) 1503 | 1504 | # Commit transaction 1505 | db.commit() 1506 | 1507 | # Return created item 1508 | return PocketItem( 1509 | id=item_id, 1510 | created=timestamp, 1511 | text=text, 1512 | tags=normalized_tags 1513 | ) 1514 | except Exception as e: 1515 | logger.error(f"Error adding item from file: {e}") 1516 | raise 1517 | finally: 1518 | db.close() 1519 | </file> 1520 | 1521 | <file path="src/mcp_server_pocket_pick/modules/functionality/add.py"> 1522 | import sqlite3 1523 | import uuid 1524 | import json 1525 | from datetime import datetime 1526 | from pathlib import Path 1527 | import logging 1528 | from ..data_types import AddCommand, PocketItem 1529 | from ..init_db import init_db, normalize_tags 1530 | 1531 | logger = logging.getLogger(__name__) 1532 | 1533 | def add(command: AddCommand) -> PocketItem: 1534 | """ 1535 | Add a new item to the pocket pick database 1536 | 1537 | Args: 1538 | command: AddCommand with text, tags and db_path 1539 | 1540 | Returns: 1541 | PocketItem: The newly created item 1542 | """ 1543 | # Normalize tags 1544 | normalized_tags = normalize_tags(command.tags) 1545 | 1546 | # Generate a unique ID 1547 | item_id = str(uuid.uuid4()) 1548 | 1549 | # Get current timestamp 1550 | timestamp = datetime.now() 1551 | 1552 | # Connect to database 1553 | db = init_db(command.db_path) 1554 | 1555 | try: 1556 | # Serialize tags to JSON 1557 | tags_json = json.dumps(normalized_tags) 1558 | 1559 | # Insert item 1560 | db.execute( 1561 | "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", 1562 | (item_id, timestamp.isoformat(), command.text, tags_json) 1563 | ) 1564 | 1565 | # Commit transaction 1566 | db.commit() 1567 | 1568 | # Return created item 1569 | return PocketItem( 1570 | id=item_id, 1571 | created=timestamp, 1572 | text=command.text, 1573 | tags=normalized_tags 1574 | ) 1575 | except Exception as e: 1576 | logger.error(f"Error adding item: {e}") 1577 | raise 1578 | finally: 1579 | db.close() 1580 | </file> 1581 | 1582 | <file path="src/mcp_server_pocket_pick/modules/functionality/backup.py"> 1583 | import sqlite3 1584 | import shutil 1585 | import logging 1586 | from ..data_types import BackupCommand 1587 | from ..init_db import init_db 1588 | 1589 | logger = logging.getLogger(__name__) 1590 | 1591 | def backup(command: BackupCommand) -> bool: 1592 | """ 1593 | Backup the pocket pick database to a specified location 1594 | 1595 | Args: 1596 | command: BackupCommand with backup destination path 1597 | 1598 | Returns: 1599 | bool: True if backup was successful, False otherwise 1600 | """ 1601 | # Make sure source DB exists by initializing it if needed 1602 | db = init_db(command.db_path) 1603 | db.close() 1604 | 1605 | try: 1606 | # Create parent directories if they don't exist 1607 | command.backup_path.parent.mkdir(parents=True, exist_ok=True) 1608 | 1609 | # Copy the database file to the backup location 1610 | shutil.copy2(command.db_path, command.backup_path) 1611 | 1612 | # Verify the backup file exists 1613 | if command.backup_path.exists(): 1614 | logger.info(f"Backup created successfully at {command.backup_path}") 1615 | return True 1616 | else: 1617 | logger.error(f"Backup file not found at {command.backup_path}") 1618 | return False 1619 | except Exception as e: 1620 | logger.error(f"Error creating backup: {e}") 1621 | return False 1622 | </file> 1623 | 1624 | <file path="src/mcp_server_pocket_pick/modules/functionality/get.py"> 1625 | import sqlite3 1626 | import json 1627 | from datetime import datetime 1628 | import logging 1629 | from typing import Optional 1630 | from ..data_types import GetCommand, PocketItem 1631 | from ..init_db import init_db 1632 | 1633 | logger = logging.getLogger(__name__) 1634 | 1635 | def get(command: GetCommand) -> Optional[PocketItem]: 1636 | """ 1637 | Get an item from the pocket pick database by ID 1638 | 1639 | Args: 1640 | command: GetCommand with item ID 1641 | 1642 | Returns: 1643 | Optional[PocketItem]: The item if found, None otherwise 1644 | """ 1645 | # Connect to database 1646 | db = init_db(command.db_path) 1647 | 1648 | try: 1649 | # Query for item with given ID 1650 | cursor = db.execute( 1651 | "SELECT id, created, text, tags FROM POCKET_PICK WHERE id = ?", 1652 | (command.id,) 1653 | ) 1654 | 1655 | # Fetch the row 1656 | row = cursor.fetchone() 1657 | 1658 | # If no row was found, return None 1659 | if row is None: 1660 | return None 1661 | 1662 | # Process the row 1663 | id, created_str, text, tags_json = row 1664 | 1665 | # Parse the created timestamp 1666 | created = datetime.fromisoformat(created_str) 1667 | 1668 | # Parse the tags JSON 1669 | tags = json.loads(tags_json) 1670 | 1671 | # Create and return the item 1672 | return PocketItem( 1673 | id=id, 1674 | created=created, 1675 | text=text, 1676 | tags=tags 1677 | ) 1678 | except Exception as e: 1679 | logger.error(f"Error getting item {command.id}: {e}") 1680 | raise 1681 | finally: 1682 | db.close() 1683 | </file> 1684 | 1685 | <file path="src/mcp_server_pocket_pick/modules/functionality/list_tags.py"> 1686 | import sqlite3 1687 | import json 1688 | from typing import List, Dict 1689 | import logging 1690 | from ..data_types import ListTagsCommand 1691 | from ..init_db import init_db 1692 | 1693 | logger = logging.getLogger(__name__) 1694 | 1695 | def list_tags(command: ListTagsCommand) -> List[Dict[str, int]]: 1696 | """ 1697 | List all tags in the pocket pick database with their counts 1698 | 1699 | Args: 1700 | command: ListTagsCommand with limit 1701 | 1702 | Returns: 1703 | List[Dict[str, int]]: List of dicts with tag name and count 1704 | """ 1705 | # Connect to database 1706 | db = init_db(command.db_path) 1707 | 1708 | try: 1709 | # Get all tags with their counts 1710 | cursor = db.execute("SELECT tags FROM POCKET_PICK") 1711 | 1712 | # Process results to count tags 1713 | tag_counts = {} 1714 | for (tags_json,) in cursor.fetchall(): 1715 | tags = json.loads(tags_json) 1716 | for tag in tags: 1717 | if tag in tag_counts: 1718 | tag_counts[tag] += 1 1719 | else: 1720 | tag_counts[tag] = 1 1721 | 1722 | # Sort by count (descending) and then alphabetically 1723 | sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0])) 1724 | 1725 | # Apply limit and format result 1726 | result = [{"tag": tag, "count": count} for tag, count in sorted_tags[:command.limit]] 1727 | 1728 | return result 1729 | except Exception as e: 1730 | logger.error(f"Error listing tags: {e}") 1731 | raise 1732 | finally: 1733 | db.close() 1734 | </file> 1735 | 1736 | <file path="src/mcp_server_pocket_pick/modules/functionality/list.py"> 1737 | import sqlite3 1738 | import json 1739 | from datetime import datetime 1740 | from typing import List 1741 | import logging 1742 | from ..data_types import ListCommand, PocketItem 1743 | from ..init_db import init_db, normalize_tags 1744 | 1745 | logger = logging.getLogger(__name__) 1746 | 1747 | def list_items(command: ListCommand) -> List[PocketItem]: 1748 | """ 1749 | List items in the pocket pick database, optionally filtered by tags 1750 | 1751 | Args: 1752 | command: ListCommand with optional tag filters and limit 1753 | 1754 | Returns: 1755 | List[PocketItem]: List of matching items 1756 | """ 1757 | # Normalize tags 1758 | normalized_tags = normalize_tags(command.tags) if command.tags else [] 1759 | 1760 | # Connect to database 1761 | db = init_db(command.db_path) 1762 | 1763 | try: 1764 | # Base query 1765 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 1766 | params = [] 1767 | 1768 | # Apply tag filter if tags are specified 1769 | if normalized_tags: 1770 | # We need to check if each tag exists in the JSON array 1771 | tag_clauses = [] 1772 | for tag in normalized_tags: 1773 | tag_clauses.append("tags LIKE ?") 1774 | # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets 1775 | params.append(f"%\"{tag}\"%") 1776 | 1777 | query += f" WHERE {' AND '.join(tag_clauses)}" 1778 | 1779 | # Apply order and limit 1780 | query += f" ORDER BY created DESC LIMIT {command.limit}" 1781 | 1782 | # Execute query 1783 | cursor = db.execute(query, params) 1784 | 1785 | # Process results 1786 | results = [] 1787 | for row in cursor.fetchall(): 1788 | id, created_str, text, tags_json = row 1789 | 1790 | # Parse the created timestamp 1791 | created = datetime.fromisoformat(created_str) 1792 | 1793 | # Parse the tags JSON 1794 | tags = json.loads(tags_json) 1795 | 1796 | # Create item 1797 | item = PocketItem( 1798 | id=id, 1799 | created=created, 1800 | text=text, 1801 | tags=tags 1802 | ) 1803 | 1804 | results.append(item) 1805 | 1806 | return results 1807 | except Exception as e: 1808 | logger.error(f"Error listing items: {e}") 1809 | raise 1810 | finally: 1811 | db.close() 1812 | </file> 1813 | 1814 | <file path="src/mcp_server_pocket_pick/modules/functionality/remove.py"> 1815 | import sqlite3 1816 | import logging 1817 | from ..data_types import RemoveCommand 1818 | from ..init_db import init_db 1819 | 1820 | logger = logging.getLogger(__name__) 1821 | 1822 | def remove(command: RemoveCommand) -> bool: 1823 | """ 1824 | Remove an item from the pocket pick database by ID 1825 | 1826 | Args: 1827 | command: RemoveCommand with item ID 1828 | 1829 | Returns: 1830 | bool: True if an item was removed, False if no matching item was found 1831 | """ 1832 | # Connect to database 1833 | db = init_db(command.db_path) 1834 | 1835 | try: 1836 | # Delete item with given ID 1837 | cursor = db.execute("DELETE FROM POCKET_PICK WHERE id = ?", (command.id,)) 1838 | 1839 | # Commit the transaction 1840 | db.commit() 1841 | 1842 | # Check if any row was affected 1843 | return cursor.rowcount > 0 1844 | except Exception as e: 1845 | logger.error(f"Error removing item {command.id}: {e}") 1846 | raise 1847 | finally: 1848 | db.close() 1849 | </file> 1850 | 1851 | <file path="src/mcp_server_pocket_pick/modules/functionality/to_file_by_id.py"> 1852 | from pathlib import Path 1853 | import logging 1854 | import os 1855 | from ..data_types import ToFileByIdCommand, PocketItem 1856 | from .get import get 1857 | from .get import GetCommand 1858 | 1859 | logger = logging.getLogger(__name__) 1860 | 1861 | def to_file_by_id(command: ToFileByIdCommand) -> bool: 1862 | """ 1863 | Write pocket pick content with given ID to the specified file 1864 | 1865 | Args: 1866 | command: ToFileByIdCommand with id, output_file_path and db_path 1867 | 1868 | Returns: 1869 | bool: True if successful, False otherwise 1870 | """ 1871 | try: 1872 | # First get the item from the database 1873 | get_command = GetCommand( 1874 | id=command.id, 1875 | db_path=command.db_path 1876 | ) 1877 | 1878 | item = get(get_command) 1879 | 1880 | if not item: 1881 | logger.error(f"Item with ID {command.id} not found") 1882 | return False 1883 | 1884 | # Ensure parent directory exists 1885 | output_path = Path(command.output_file_path_abs) 1886 | output_path.parent.mkdir(parents=True, exist_ok=True) 1887 | 1888 | # Write content to file 1889 | with open(output_path, 'w', encoding='utf-8') as f: 1890 | f.write(item.text) 1891 | 1892 | return True 1893 | except Exception as e: 1894 | logger.error(f"Error writing to file {command.output_file_path_abs}: {e}") 1895 | return False 1896 | </file> 1897 | 1898 | <file path="src/mcp_server_pocket_pick/modules/__init__.py"> 1899 | # Module initialization 1900 | </file> 1901 | 1902 | <file path="src/mcp_server_pocket_pick/modules/constants.py"> 1903 | from pathlib import Path 1904 | 1905 | DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" 1906 | </file> 1907 | 1908 | <file path="src/mcp_server_pocket_pick/tests/functionality/__init__.py"> 1909 | # Functionality tests package initialization 1910 | </file> 1911 | 1912 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_add_file.py"> 1913 | import pytest 1914 | import tempfile 1915 | import os 1916 | from pathlib import Path 1917 | import json 1918 | import sqlite3 1919 | from ...modules.data_types import AddFileCommand, PocketItem 1920 | from ...modules.functionality.add_file import add_file 1921 | 1922 | @pytest.fixture 1923 | def temp_db_path(): 1924 | # Create a temporary file path 1925 | fd, path = tempfile.mkstemp() 1926 | os.close(fd) 1927 | 1928 | # Return the path as a Path object 1929 | yield Path(path) 1930 | 1931 | # Clean up the temp file after test 1932 | if os.path.exists(path): 1933 | os.unlink(path) 1934 | 1935 | @pytest.fixture 1936 | def temp_file_with_content(): 1937 | # Create a temporary file with content 1938 | fd, path = tempfile.mkstemp() 1939 | with os.fdopen(fd, 'w') as file: 1940 | file.write("This is test content from a file") 1941 | 1942 | # Return the path as a string 1943 | yield path 1944 | 1945 | # Clean up the temp file after test 1946 | if os.path.exists(path): 1947 | os.unlink(path) 1948 | 1949 | def test_add_file_simple(temp_db_path, temp_file_with_content): 1950 | # Create a command to add a file content 1951 | command = AddFileCommand( 1952 | file_path=temp_file_with_content, 1953 | tags=["test", "file"], 1954 | db_path=temp_db_path 1955 | ) 1956 | 1957 | # Add the item 1958 | result = add_file(command) 1959 | 1960 | # Verify result is a PocketItem 1961 | assert isinstance(result, PocketItem) 1962 | assert result.text == "This is test content from a file" 1963 | assert result.tags == ["test", "file"] 1964 | assert result.id is not None 1965 | 1966 | # Verify item was added to the database 1967 | db = sqlite3.connect(temp_db_path) 1968 | cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") 1969 | row = cursor.fetchone() 1970 | 1971 | assert row is not None 1972 | assert row[0] == result.id 1973 | assert row[1] == "This is test content from a file" 1974 | 1975 | # Verify tags were stored as JSON 1976 | stored_tags = json.loads(row[2]) 1977 | assert stored_tags == ["test", "file"] 1978 | 1979 | # Verify no more rows exist 1980 | assert cursor.fetchone() is None 1981 | 1982 | db.close() 1983 | 1984 | def test_add_file_with_tag_normalization(temp_db_path, temp_file_with_content): 1985 | # Create a command with tags that need normalization 1986 | command = AddFileCommand( 1987 | file_path=temp_file_with_content, 1988 | tags=["FILE", "with space", "under_score"], 1989 | db_path=temp_db_path 1990 | ) 1991 | 1992 | # Add the item 1993 | result = add_file(command) 1994 | 1995 | # Verify tags were normalized 1996 | assert result.tags == ["file", "with-space", "under-score"] 1997 | 1998 | # Verify in database 1999 | db = sqlite3.connect(temp_db_path) 2000 | cursor = db.execute("SELECT tags FROM POCKET_PICK") 2001 | row = cursor.fetchone() 2002 | 2003 | stored_tags = json.loads(row[0]) 2004 | assert stored_tags == ["file", "with-space", "under-score"] 2005 | 2006 | db.close() 2007 | 2008 | def test_add_file_nonexistent(temp_db_path): 2009 | # Create a command with a nonexistent file 2010 | command = AddFileCommand( 2011 | file_path="/nonexistent/file/path.txt", 2012 | tags=["test"], 2013 | db_path=temp_db_path 2014 | ) 2015 | 2016 | # Expect FileNotFoundError when adding 2017 | with pytest.raises(FileNotFoundError): 2018 | add_file(command) 2019 | </file> 2020 | 2021 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_add.py"> 2022 | import pytest 2023 | import tempfile 2024 | import os 2025 | from pathlib import Path 2026 | import json 2027 | import sqlite3 2028 | from ...modules.data_types import AddCommand, PocketItem 2029 | from ...modules.functionality.add import add 2030 | 2031 | @pytest.fixture 2032 | def temp_db_path(): 2033 | # Create a temporary file path 2034 | fd, path = tempfile.mkstemp() 2035 | os.close(fd) 2036 | 2037 | # Return the path as a Path object 2038 | yield Path(path) 2039 | 2040 | # Clean up the temp file after test 2041 | if os.path.exists(path): 2042 | os.unlink(path) 2043 | 2044 | def test_add_simple(temp_db_path): 2045 | # Create a command to add a simple item 2046 | command = AddCommand( 2047 | text="This is a test item", 2048 | tags=["test", "example"], 2049 | db_path=temp_db_path 2050 | ) 2051 | 2052 | # Add the item 2053 | result = add(command) 2054 | 2055 | # Verify result is a PocketItem 2056 | assert isinstance(result, PocketItem) 2057 | assert result.text == "This is a test item" 2058 | assert result.tags == ["test", "example"] 2059 | assert result.id is not None 2060 | 2061 | # Verify item was added to the database 2062 | db = sqlite3.connect(temp_db_path) 2063 | cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") 2064 | row = cursor.fetchone() 2065 | 2066 | assert row is not None 2067 | assert row[0] == result.id 2068 | assert row[1] == "This is a test item" 2069 | 2070 | # Verify tags were stored as JSON 2071 | stored_tags = json.loads(row[2]) 2072 | assert stored_tags == ["test", "example"] 2073 | 2074 | # Verify no more rows exist 2075 | assert cursor.fetchone() is None 2076 | 2077 | db.close() 2078 | 2079 | def test_add_with_tag_normalization(temp_db_path): 2080 | # Create a command with tags that need normalization 2081 | command = AddCommand( 2082 | text="Item with tags to normalize", 2083 | tags=["TAG", "with space", "under_score"], 2084 | db_path=temp_db_path 2085 | ) 2086 | 2087 | # Add the item 2088 | result = add(command) 2089 | 2090 | # Verify tags were normalized 2091 | assert result.tags == ["tag", "with-space", "under-score"] 2092 | 2093 | # Verify in database 2094 | db = sqlite3.connect(temp_db_path) 2095 | cursor = db.execute("SELECT tags FROM POCKET_PICK") 2096 | row = cursor.fetchone() 2097 | 2098 | stored_tags = json.loads(row[0]) 2099 | assert stored_tags == ["tag", "with-space", "under-score"] 2100 | 2101 | db.close() 2102 | </file> 2103 | 2104 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_backup.py"> 2105 | import pytest 2106 | import tempfile 2107 | import os 2108 | import sqlite3 2109 | from pathlib import Path 2110 | from ...modules.data_types import AddCommand, BackupCommand 2111 | from ...modules.functionality.add import add 2112 | from ...modules.functionality.backup import backup 2113 | 2114 | @pytest.fixture 2115 | def temp_db_path(): 2116 | # Create a temporary file path 2117 | fd, path = tempfile.mkstemp() 2118 | os.close(fd) 2119 | 2120 | # Return the path as a Path object 2121 | yield Path(path) 2122 | 2123 | # Clean up the temp file after test 2124 | if os.path.exists(path): 2125 | os.unlink(path) 2126 | 2127 | @pytest.fixture 2128 | def temp_backup_path(): 2129 | # Create a temporary file path for backup 2130 | fd, path = tempfile.mkstemp() 2131 | os.close(fd) 2132 | os.unlink(path) # Remove the file so backup can create it 2133 | 2134 | # Return the path as a Path object 2135 | yield Path(path) 2136 | 2137 | # Clean up the temp file after test 2138 | if os.path.exists(path): 2139 | os.unlink(path) 2140 | 2141 | @pytest.fixture 2142 | def populated_db(temp_db_path): 2143 | # Add a test item to the database 2144 | command = AddCommand( 2145 | text="Test item for backup", 2146 | tags=["test", "backup"], 2147 | db_path=temp_db_path 2148 | ) 2149 | 2150 | add(command) 2151 | return temp_db_path 2152 | 2153 | def test_backup_success(populated_db, temp_backup_path): 2154 | # Backup the database 2155 | command = BackupCommand( 2156 | backup_path=temp_backup_path, 2157 | db_path=populated_db 2158 | ) 2159 | 2160 | result = backup(command) 2161 | 2162 | # Should return True indicating success 2163 | assert result is True 2164 | 2165 | # Verify backup file exists 2166 | assert temp_backup_path.exists() 2167 | 2168 | # Verify backup contains the same data as original 2169 | original_db = sqlite3.connect(populated_db) 2170 | original_cursor = original_db.execute("SELECT id, text, tags FROM POCKET_PICK") 2171 | original_row = original_cursor.fetchone() 2172 | original_db.close() 2173 | 2174 | backup_db = sqlite3.connect(temp_backup_path) 2175 | backup_cursor = backup_db.execute("SELECT id, text, tags FROM POCKET_PICK") 2176 | backup_row = backup_cursor.fetchone() 2177 | backup_db.close() 2178 | 2179 | assert backup_row is not None 2180 | assert backup_row[0] == original_row[0] # ID 2181 | assert backup_row[1] == original_row[1] # text 2182 | assert backup_row[2] == original_row[2] # tags 2183 | 2184 | def test_backup_nested_directory_creation(populated_db): 2185 | # Create a backup path in a nested directory that doesn't exist 2186 | with tempfile.TemporaryDirectory() as temp_dir: 2187 | nested_dir = Path(temp_dir) / "nested" / "dirs" 2188 | backup_path = nested_dir / "backup.db" 2189 | 2190 | # Backup the database 2191 | command = BackupCommand( 2192 | backup_path=backup_path, 2193 | db_path=populated_db 2194 | ) 2195 | 2196 | result = backup(command) 2197 | 2198 | # Should return True indicating success 2199 | assert result is True 2200 | 2201 | # Verify backup file exists 2202 | assert backup_path.exists() 2203 | 2204 | def test_backup_from_nonexistent_db(temp_db_path, temp_backup_path): 2205 | # Try to backup from a nonexistent database 2206 | # (temp_db_path fixture exists but is empty) 2207 | command = BackupCommand( 2208 | backup_path=temp_backup_path, 2209 | db_path=temp_db_path 2210 | ) 2211 | 2212 | result = backup(command) 2213 | 2214 | # Should return True indicating success (empty database created and backed up) 2215 | assert result is True 2216 | assert temp_backup_path.exists() 2217 | </file> 2218 | 2219 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_list_tags.py"> 2220 | import pytest 2221 | import tempfile 2222 | import os 2223 | from pathlib import Path 2224 | from ...modules.data_types import AddCommand, ListTagsCommand 2225 | from ...modules.functionality.add import add 2226 | from ...modules.functionality.list_tags import list_tags 2227 | 2228 | @pytest.fixture 2229 | def temp_db_path(): 2230 | # Create a temporary file path 2231 | fd, path = tempfile.mkstemp() 2232 | os.close(fd) 2233 | 2234 | # Return the path as a Path object 2235 | yield Path(path) 2236 | 2237 | # Clean up the temp file after test 2238 | if os.path.exists(path): 2239 | os.unlink(path) 2240 | 2241 | @pytest.fixture 2242 | def populated_db(temp_db_path): 2243 | # Create sample items 2244 | items = [ 2245 | {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 2246 | {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 2247 | {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 2248 | {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 2249 | {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 2250 | ] 2251 | 2252 | # Add items to the database 2253 | for item in items: 2254 | command = AddCommand( 2255 | text=item["text"], 2256 | tags=item["tags"], 2257 | db_path=temp_db_path 2258 | ) 2259 | add(command) 2260 | 2261 | return temp_db_path 2262 | 2263 | def test_list_tags_all(populated_db): 2264 | # List all tags 2265 | command = ListTagsCommand( 2266 | db_path=populated_db 2267 | ) 2268 | 2269 | results = list_tags(command) 2270 | 2271 | # Verify all expected tags are present 2272 | tags = [result["tag"] for result in results] 2273 | expected_tags = [ 2274 | "programming", # Count: 4 2275 | "fun", # Count: 2 2276 | "python", # Count: 1 2277 | "sql", # Count: 1 2278 | "database", # Count: 1 2279 | "testing", # Count: 1 2280 | "code", # Count: 1 2281 | "regex", # Count: 1 2282 | "advanced", # Count: 1 2283 | "learning", # Count: 1 2284 | "technology" # Count: 1 2285 | ] 2286 | 2287 | for expected in expected_tags: 2288 | assert expected in tags 2289 | 2290 | # Verify the most common tag is first (sorted by count) 2291 | assert results[0]["tag"] == "programming" 2292 | assert results[0]["count"] == 4 2293 | 2294 | # Verify second most common tag 2295 | assert results[1]["tag"] == "fun" 2296 | assert results[1]["count"] == 2 2297 | 2298 | def test_list_tags_limit(populated_db): 2299 | # List tags with limit 2300 | command = ListTagsCommand( 2301 | limit=3, 2302 | db_path=populated_db 2303 | ) 2304 | 2305 | results = list_tags(command) 2306 | 2307 | # Should only return 3 tags 2308 | assert len(results) == 3 2309 | 2310 | # Verify the top 3 tags in order by count 2311 | # (with ties broken alphabetically) 2312 | assert results[0]["tag"] == "programming" 2313 | assert results[1]["tag"] == "fun" 2314 | 2315 | # The third item should be one of the single-count tags 2316 | assert results[2]["count"] == 1 2317 | 2318 | def test_list_tags_empty_db(temp_db_path): 2319 | # List tags in empty database 2320 | command = ListTagsCommand( 2321 | db_path=temp_db_path 2322 | ) 2323 | 2324 | results = list_tags(command) 2325 | 2326 | # Should return empty list 2327 | assert len(results) == 0 2328 | </file> 2329 | 2330 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_list.py"> 2331 | import pytest 2332 | import tempfile 2333 | import os 2334 | from pathlib import Path 2335 | from ...modules.data_types import AddCommand, ListCommand 2336 | from ...modules.functionality.add import add 2337 | from ...modules.functionality.list import list_items 2338 | 2339 | @pytest.fixture 2340 | def temp_db_path(): 2341 | # Create a temporary file path 2342 | fd, path = tempfile.mkstemp() 2343 | os.close(fd) 2344 | 2345 | # Return the path as a Path object 2346 | yield Path(path) 2347 | 2348 | # Clean up the temp file after test 2349 | if os.path.exists(path): 2350 | os.unlink(path) 2351 | 2352 | @pytest.fixture 2353 | def populated_db(temp_db_path): 2354 | # Create sample items 2355 | items = [ 2356 | {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 2357 | {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 2358 | {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 2359 | {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 2360 | {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 2361 | ] 2362 | 2363 | # Add items to the database 2364 | for item in items: 2365 | command = AddCommand( 2366 | text=item["text"], 2367 | tags=item["tags"], 2368 | db_path=temp_db_path 2369 | ) 2370 | add(command) 2371 | 2372 | return temp_db_path 2373 | 2374 | def test_list_all(populated_db): 2375 | # List all items 2376 | command = ListCommand( 2377 | limit=10, 2378 | db_path=populated_db 2379 | ) 2380 | 2381 | results = list_items(command) 2382 | 2383 | # Should return all 5 items 2384 | assert len(results) == 5 2385 | 2386 | # Check that all expected texts are present 2387 | texts = [result.text for result in results] 2388 | expected_texts = [ 2389 | "Python programming is fun", 2390 | "SQL databases are powerful", 2391 | "Testing code is important", 2392 | "Regular expressions can be complex", 2393 | "Learning new technologies is exciting" 2394 | ] 2395 | 2396 | for expected in expected_texts: 2397 | assert expected in texts 2398 | 2399 | def test_list_with_tags(populated_db): 2400 | # List items with specific tag 2401 | command = ListCommand( 2402 | tags=["programming"], 2403 | limit=10, 2404 | db_path=populated_db 2405 | ) 2406 | 2407 | results = list_items(command) 2408 | 2409 | # Should return items with the "programming" tag (4 items) 2410 | assert len(results) == 4 2411 | 2412 | # Verify the correct items are returned 2413 | texts = [result.text for result in results] 2414 | expected_texts = [ 2415 | "Python programming is fun", 2416 | "SQL databases are powerful", 2417 | "Testing code is important", 2418 | "Regular expressions can be complex" 2419 | ] 2420 | 2421 | for expected in expected_texts: 2422 | assert expected in texts 2423 | 2424 | def test_list_with_multiple_tags(populated_db): 2425 | # List items with multiple tags 2426 | command = ListCommand( 2427 | tags=["programming", "fun"], 2428 | limit=10, 2429 | db_path=populated_db 2430 | ) 2431 | 2432 | results = list_items(command) 2433 | 2434 | # Should return items with both "programming" and "fun" tags (1 item) 2435 | assert len(results) == 1 2436 | assert results[0].text == "Python programming is fun" 2437 | 2438 | def test_list_limit(populated_db): 2439 | # List with limit 2440 | command = ListCommand( 2441 | limit=2, 2442 | db_path=populated_db 2443 | ) 2444 | 2445 | results = list_items(command) 2446 | 2447 | # Should only return 2 items 2448 | assert len(results) == 2 2449 | </file> 2450 | 2451 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_remove_get.py"> 2452 | import pytest 2453 | import tempfile 2454 | import os 2455 | from pathlib import Path 2456 | import sqlite3 2457 | from ...modules.data_types import AddCommand, RemoveCommand, GetCommand 2458 | from ...modules.functionality.add import add 2459 | from ...modules.functionality.remove import remove 2460 | from ...modules.functionality.get import get 2461 | 2462 | @pytest.fixture 2463 | def temp_db_path(): 2464 | # Create a temporary file path 2465 | fd, path = tempfile.mkstemp() 2466 | os.close(fd) 2467 | 2468 | # Return the path as a Path object 2469 | yield Path(path) 2470 | 2471 | # Clean up the temp file after test 2472 | if os.path.exists(path): 2473 | os.unlink(path) 2474 | 2475 | @pytest.fixture 2476 | def item_id(temp_db_path): 2477 | # Add a test item and return its ID 2478 | command = AddCommand( 2479 | text="Test item for get and remove", 2480 | tags=["test", "example"], 2481 | db_path=temp_db_path 2482 | ) 2483 | 2484 | result = add(command) 2485 | return result.id, temp_db_path 2486 | 2487 | def test_get_item(item_id): 2488 | id, db_path = item_id 2489 | 2490 | # Get the item by ID 2491 | command = GetCommand( 2492 | id=id, 2493 | db_path=db_path 2494 | ) 2495 | 2496 | result = get(command) 2497 | 2498 | # Verify item properties 2499 | assert result is not None 2500 | assert result.id == id 2501 | assert result.text == "Test item for get and remove" 2502 | assert set(result.tags) == set(["test", "example"]) 2503 | 2504 | def test_get_nonexistent_item(temp_db_path): 2505 | # Try to get a nonexistent item 2506 | command = GetCommand( 2507 | id="nonexistent-id", 2508 | db_path=temp_db_path 2509 | ) 2510 | 2511 | result = get(command) 2512 | 2513 | # Should return None 2514 | assert result is None 2515 | 2516 | def test_remove_item(item_id): 2517 | id, db_path = item_id 2518 | 2519 | # Remove the item 2520 | command = RemoveCommand( 2521 | id=id, 2522 | db_path=db_path 2523 | ) 2524 | 2525 | result = remove(command) 2526 | 2527 | # Should return True indicating success 2528 | assert result is True 2529 | 2530 | # Verify item is no longer in the database 2531 | db = sqlite3.connect(db_path) 2532 | cursor = db.execute("SELECT COUNT(*) FROM POCKET_PICK WHERE id = ?", (id,)) 2533 | count = cursor.fetchone()[0] 2534 | db.close() 2535 | 2536 | assert count == 0 2537 | 2538 | # Trying to get the removed item should return None 2539 | get_command = GetCommand( 2540 | id=id, 2541 | db_path=db_path 2542 | ) 2543 | get_result = get(get_command) 2544 | assert get_result is None 2545 | 2546 | def test_remove_nonexistent_item(temp_db_path): 2547 | # Try to remove a nonexistent item 2548 | command = RemoveCommand( 2549 | id="nonexistent-id", 2550 | db_path=temp_db_path 2551 | ) 2552 | 2553 | result = remove(command) 2554 | 2555 | # Should return False indicating failure 2556 | assert result is False 2557 | </file> 2558 | 2559 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_to_file_by_id.py"> 2560 | import pytest 2561 | import tempfile 2562 | import os 2563 | from pathlib import Path 2564 | import json 2565 | import sqlite3 2566 | from ...modules.data_types import AddCommand, ToFileByIdCommand, PocketItem 2567 | from ...modules.functionality.add import add 2568 | from ...modules.functionality.to_file_by_id import to_file_by_id 2569 | 2570 | @pytest.fixture 2571 | def temp_db_path(): 2572 | # Create a temporary file path 2573 | fd, path = tempfile.mkstemp() 2574 | os.close(fd) 2575 | 2576 | # Return the path as a Path object 2577 | yield Path(path) 2578 | 2579 | # Clean up the temp file after test 2580 | if os.path.exists(path): 2581 | os.unlink(path) 2582 | 2583 | @pytest.fixture 2584 | def sample_item(temp_db_path): 2585 | # Add a sample item to the database and return it 2586 | command = AddCommand( 2587 | text="This is sample content for testing to_file_by_id function", 2588 | tags=["test", "sample"], 2589 | db_path=temp_db_path 2590 | ) 2591 | 2592 | return add(command) 2593 | 2594 | def test_to_file_by_id_successful(temp_db_path, sample_item): 2595 | # Create a temporary output file path 2596 | fd, output_path = tempfile.mkstemp() 2597 | os.close(fd) 2598 | os.unlink(output_path) # Remove the file so we can test creation 2599 | 2600 | try: 2601 | # Create command to write content to file 2602 | command = ToFileByIdCommand( 2603 | id=sample_item.id, 2604 | output_file_path_abs=output_path, 2605 | db_path=temp_db_path 2606 | ) 2607 | 2608 | # Write content to file 2609 | result = to_file_by_id(command) 2610 | 2611 | # Verify result is True 2612 | assert result is True 2613 | 2614 | # Verify file was created with correct content 2615 | assert os.path.exists(output_path) 2616 | with open(output_path, 'r', encoding='utf-8') as f: 2617 | content = f.read() 2618 | 2619 | assert content == sample_item.text 2620 | finally: 2621 | # Clean up the temp file 2622 | if os.path.exists(output_path): 2623 | os.unlink(output_path) 2624 | 2625 | def test_to_file_by_id_nonexistent_id(temp_db_path): 2626 | # Create a temporary output file path 2627 | fd, output_path = tempfile.mkstemp() 2628 | os.close(fd) 2629 | os.unlink(output_path) # Remove the file so we can test creation 2630 | 2631 | try: 2632 | # Create command with non-existent ID 2633 | command = ToFileByIdCommand( 2634 | id="nonexistent-id", 2635 | output_file_path_abs=output_path, 2636 | db_path=temp_db_path 2637 | ) 2638 | 2639 | # Attempt to write content to file 2640 | result = to_file_by_id(command) 2641 | 2642 | # Verify result is False 2643 | assert result is False 2644 | 2645 | # Verify file was not created 2646 | assert not os.path.exists(output_path) 2647 | finally: 2648 | # Clean up the temp file if it was created 2649 | if os.path.exists(output_path): 2650 | os.unlink(output_path) 2651 | 2652 | def test_to_file_by_id_creates_directories(temp_db_path, sample_item): 2653 | # Create a temporary directory 2654 | temp_dir = tempfile.mkdtemp() 2655 | try: 2656 | # Create a path with nested directories that don't exist 2657 | output_path = os.path.join(temp_dir, "nested", "dirs", "output.txt") 2658 | 2659 | # Create command to write content to file 2660 | command = ToFileByIdCommand( 2661 | id=sample_item.id, 2662 | output_file_path_abs=output_path, 2663 | db_path=temp_db_path 2664 | ) 2665 | 2666 | # Write content to file 2667 | result = to_file_by_id(command) 2668 | 2669 | # Verify result is True 2670 | assert result is True 2671 | 2672 | # Verify file was created with correct content 2673 | assert os.path.exists(output_path) 2674 | with open(output_path, 'r', encoding='utf-8') as f: 2675 | content = f.read() 2676 | 2677 | assert content == sample_item.text 2678 | finally: 2679 | # Clean up the temp dir 2680 | import shutil 2681 | shutil.rmtree(temp_dir) 2682 | 2683 | def test_to_file_by_id_handles_errors(temp_db_path, sample_item, monkeypatch): 2684 | # Mock the open function to raise a PermissionError 2685 | def mock_open_with_permission_error(*args, **kwargs): 2686 | raise PermissionError("Permission denied") 2687 | 2688 | # Create a temporary output file path 2689 | fd, output_path = tempfile.mkstemp() 2690 | os.close(fd) 2691 | os.unlink(output_path) 2692 | 2693 | try: 2694 | # Create command to write content to file 2695 | command = ToFileByIdCommand( 2696 | id=sample_item.id, 2697 | output_file_path_abs=output_path, 2698 | db_path=temp_db_path 2699 | ) 2700 | 2701 | # Monkeypatch the built-in open function 2702 | monkeypatch.setattr("builtins.open", mock_open_with_permission_error) 2703 | 2704 | # Attempt to write content to file 2705 | result = to_file_by_id(command) 2706 | 2707 | # Verify result is False because of permission error 2708 | assert result is False 2709 | finally: 2710 | # Clean up the temp file if it was created 2711 | if os.path.exists(output_path): 2712 | os.unlink(output_path) 2713 | </file> 2714 | 2715 | <file path="src/mcp_server_pocket_pick/tests/__init__.py"> 2716 | # Tests package initialization 2717 | </file> 2718 | 2719 | <file path="src/mcp_server_pocket_pick/tests/test_init_db.py"> 2720 | import pytest 2721 | import tempfile 2722 | import os 2723 | from pathlib import Path 2724 | import sqlite3 2725 | from ..modules.init_db import init_db, normalize_tag, normalize_tags 2726 | 2727 | def test_init_db(): 2728 | # Create a temporary file path 2729 | fd, path = tempfile.mkstemp() 2730 | os.close(fd) 2731 | 2732 | try: 2733 | # Initialize database with the temp path 2734 | db_path = Path(path) 2735 | db = init_db(db_path) 2736 | 2737 | # Verify connection is open 2738 | assert isinstance(db, sqlite3.Connection) 2739 | 2740 | # Verify POCKET_PICK table exists 2741 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='POCKET_PICK'") 2742 | assert cursor.fetchone() is not None 2743 | 2744 | # Verify indexes exist 2745 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_created'") 2746 | assert cursor.fetchone() is not None 2747 | 2748 | cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_text'") 2749 | assert cursor.fetchone() is not None 2750 | 2751 | # Close the connection 2752 | db.close() 2753 | finally: 2754 | # Clean up the temp file 2755 | if os.path.exists(path): 2756 | os.unlink(path) 2757 | 2758 | def test_normalize_tag(): 2759 | # Test lowercase conversion 2760 | assert normalize_tag("TAG") == "tag" 2761 | 2762 | # Test whitespace trimming 2763 | assert normalize_tag(" tag ") == "tag" 2764 | 2765 | # Test space replacement 2766 | assert normalize_tag("my tag") == "my-tag" 2767 | 2768 | # Test underscore replacement 2769 | assert normalize_tag("my_tag") == "my-tag" 2770 | 2771 | # Test combined operations 2772 | assert normalize_tag(" MY_TAG with SPACES ") == "my-tag-with-spaces" 2773 | 2774 | def test_normalize_tags(): 2775 | tags = ["TAG1", " tag2 ", "my_tag3", "My Tag4"] 2776 | normalized = normalize_tags(tags) 2777 | 2778 | assert normalized == ["tag1", "tag2", "my-tag3", "my-tag4"] 2779 | </file> 2780 | 2781 | <file path="src/mcp_server_pocket_pick/__main__.py"> 2782 | from mcp_server_pocket_pick import main 2783 | 2784 | main() 2785 | </file> 2786 | 2787 | <file path=".python-version"> 2788 | 3.10 2789 | </file> 2790 | 2791 | <file path="uv.lock"> 2792 | version = 1 2793 | requires-python = ">=3.10" 2794 | 2795 | [[package]] 2796 | name = "annotated-types" 2797 | version = "0.7.0" 2798 | source = { registry = "https://pypi.org/simple" } 2799 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 2800 | wheels = [ 2801 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 2802 | ] 2803 | 2804 | [[package]] 2805 | name = "anyio" 2806 | version = "4.8.0" 2807 | source = { registry = "https://pypi.org/simple" } 2808 | dependencies = [ 2809 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 2810 | { name = "idna" }, 2811 | { name = "sniffio" }, 2812 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 2813 | ] 2814 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 2815 | wheels = [ 2816 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 2817 | ] 2818 | 2819 | [[package]] 2820 | name = "certifi" 2821 | version = "2025.1.31" 2822 | source = { registry = "https://pypi.org/simple" } 2823 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 2824 | wheels = [ 2825 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 2826 | ] 2827 | 2828 | [[package]] 2829 | name = "click" 2830 | version = "8.1.8" 2831 | source = { registry = "https://pypi.org/simple" } 2832 | dependencies = [ 2833 | { name = "colorama", marker = "sys_platform == 'win32'" }, 2834 | ] 2835 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 2836 | wheels = [ 2837 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 2838 | ] 2839 | 2840 | [[package]] 2841 | name = "colorama" 2842 | version = "0.4.6" 2843 | source = { registry = "https://pypi.org/simple" } 2844 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 2845 | wheels = [ 2846 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 2847 | ] 2848 | 2849 | [[package]] 2850 | name = "exceptiongroup" 2851 | version = "1.2.2" 2852 | source = { registry = "https://pypi.org/simple" } 2853 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 2854 | wheels = [ 2855 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 2856 | ] 2857 | 2858 | [[package]] 2859 | name = "h11" 2860 | version = "0.14.0" 2861 | source = { registry = "https://pypi.org/simple" } 2862 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 2863 | wheels = [ 2864 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 2865 | ] 2866 | 2867 | [[package]] 2868 | name = "httpcore" 2869 | version = "1.0.7" 2870 | source = { registry = "https://pypi.org/simple" } 2871 | dependencies = [ 2872 | { name = "certifi" }, 2873 | { name = "h11" }, 2874 | ] 2875 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 2876 | wheels = [ 2877 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 2878 | ] 2879 | 2880 | [[package]] 2881 | name = "httpx" 2882 | version = "0.28.1" 2883 | source = { registry = "https://pypi.org/simple" } 2884 | dependencies = [ 2885 | { name = "anyio" }, 2886 | { name = "certifi" }, 2887 | { name = "httpcore" }, 2888 | { name = "idna" }, 2889 | ] 2890 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 2891 | wheels = [ 2892 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 2893 | ] 2894 | 2895 | [[package]] 2896 | name = "httpx-sse" 2897 | version = "0.4.0" 2898 | source = { registry = "https://pypi.org/simple" } 2899 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 2900 | wheels = [ 2901 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 2902 | ] 2903 | 2904 | [[package]] 2905 | name = "idna" 2906 | version = "3.10" 2907 | source = { registry = "https://pypi.org/simple" } 2908 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 2909 | wheels = [ 2910 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 2911 | ] 2912 | 2913 | [[package]] 2914 | name = "iniconfig" 2915 | version = "2.0.0" 2916 | source = { registry = "https://pypi.org/simple" } 2917 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 2918 | wheels = [ 2919 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 2920 | ] 2921 | 2922 | [[package]] 2923 | name = "mcp" 2924 | version = "1.3.0" 2925 | source = { registry = "https://pypi.org/simple" } 2926 | dependencies = [ 2927 | { name = "anyio" }, 2928 | { name = "httpx" }, 2929 | { name = "httpx-sse" }, 2930 | { name = "pydantic" }, 2931 | { name = "pydantic-settings" }, 2932 | { name = "sse-starlette" }, 2933 | { name = "starlette" }, 2934 | { name = "uvicorn" }, 2935 | ] 2936 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 } 2937 | wheels = [ 2938 | { url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 }, 2939 | ] 2940 | 2941 | [[package]] 2942 | name = "mcp-server-pocket-pick" 2943 | version = "0.1.0" 2944 | source = { editable = "." } 2945 | dependencies = [ 2946 | { name = "click" }, 2947 | { name = "mcp" }, 2948 | { name = "pydantic" }, 2949 | ] 2950 | 2951 | [package.dev-dependencies] 2952 | dev = [ 2953 | { name = "pytest" }, 2954 | ] 2955 | 2956 | [package.metadata] 2957 | requires-dist = [ 2958 | { name = "click", specifier = ">=8.1.7" }, 2959 | { name = "mcp", specifier = ">=1.0.0" }, 2960 | { name = "pydantic", specifier = ">=2.0.0" }, 2961 | ] 2962 | 2963 | [package.metadata.requires-dev] 2964 | dev = [{ name = "pytest", specifier = ">=8.0.0" }] 2965 | 2966 | [[package]] 2967 | name = "packaging" 2968 | version = "24.2" 2969 | source = { registry = "https://pypi.org/simple" } 2970 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 2971 | wheels = [ 2972 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 2973 | ] 2974 | 2975 | [[package]] 2976 | name = "pluggy" 2977 | version = "1.5.0" 2978 | source = { registry = "https://pypi.org/simple" } 2979 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 2980 | wheels = [ 2981 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 2982 | ] 2983 | 2984 | [[package]] 2985 | name = "pydantic" 2986 | version = "2.10.6" 2987 | source = { registry = "https://pypi.org/simple" } 2988 | dependencies = [ 2989 | { name = "annotated-types" }, 2990 | { name = "pydantic-core" }, 2991 | { name = "typing-extensions" }, 2992 | ] 2993 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 2994 | wheels = [ 2995 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 2996 | ] 2997 | 2998 | [[package]] 2999 | name = "pydantic-core" 3000 | version = "2.27.2" 3001 | source = { registry = "https://pypi.org/simple" } 3002 | dependencies = [ 3003 | { name = "typing-extensions" }, 3004 | ] 3005 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 3006 | wheels = [ 3007 | { 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 }, 3008 | { 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 }, 3009 | { 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 }, 3010 | { 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 }, 3011 | { 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 }, 3012 | { 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 }, 3013 | { 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 }, 3014 | { 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 }, 3015 | { 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 }, 3016 | { 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 }, 3017 | { 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 }, 3018 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 3019 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 3020 | { 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 }, 3021 | { 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 }, 3022 | { 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 }, 3023 | { 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 }, 3024 | { 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 }, 3025 | { 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 }, 3026 | { 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 }, 3027 | { 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 }, 3028 | { 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 }, 3029 | { 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 }, 3030 | { 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 }, 3031 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 3032 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 3033 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 3034 | { 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 }, 3035 | { 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 }, 3036 | { 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 }, 3037 | { 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 }, 3038 | { 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 }, 3039 | { 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 }, 3040 | { 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 }, 3041 | { 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 }, 3042 | { 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 }, 3043 | { 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 }, 3044 | { 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 }, 3045 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 3046 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 3047 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 3048 | { 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 }, 3049 | { 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 }, 3050 | { 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 }, 3051 | { 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 }, 3052 | { 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 }, 3053 | { 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 }, 3054 | { 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 }, 3055 | { 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 }, 3056 | { 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 }, 3057 | { 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 }, 3058 | { 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 }, 3059 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 3060 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 3061 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 3062 | { 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 }, 3063 | { 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 }, 3064 | { 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 }, 3065 | { 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 }, 3066 | { 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 }, 3067 | { 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 }, 3068 | { 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 }, 3069 | { 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 }, 3070 | { 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 }, 3071 | ] 3072 | 3073 | [[package]] 3074 | name = "pydantic-settings" 3075 | version = "2.8.1" 3076 | source = { registry = "https://pypi.org/simple" } 3077 | dependencies = [ 3078 | { name = "pydantic" }, 3079 | { name = "python-dotenv" }, 3080 | ] 3081 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 3082 | wheels = [ 3083 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 3084 | ] 3085 | 3086 | [[package]] 3087 | name = "pytest" 3088 | version = "8.3.5" 3089 | source = { registry = "https://pypi.org/simple" } 3090 | dependencies = [ 3091 | { name = "colorama", marker = "sys_platform == 'win32'" }, 3092 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 3093 | { name = "iniconfig" }, 3094 | { name = "packaging" }, 3095 | { name = "pluggy" }, 3096 | { name = "tomli", marker = "python_full_version < '3.11'" }, 3097 | ] 3098 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 3099 | wheels = [ 3100 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 3101 | ] 3102 | 3103 | [[package]] 3104 | name = "python-dotenv" 3105 | version = "1.0.1" 3106 | source = { registry = "https://pypi.org/simple" } 3107 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 3108 | wheels = [ 3109 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 3110 | ] 3111 | 3112 | [[package]] 3113 | name = "sniffio" 3114 | version = "1.3.1" 3115 | source = { registry = "https://pypi.org/simple" } 3116 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 3117 | wheels = [ 3118 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 3119 | ] 3120 | 3121 | [[package]] 3122 | name = "sse-starlette" 3123 | version = "2.2.1" 3124 | source = { registry = "https://pypi.org/simple" } 3125 | dependencies = [ 3126 | { name = "anyio" }, 3127 | { name = "starlette" }, 3128 | ] 3129 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 3130 | wheels = [ 3131 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 3132 | ] 3133 | 3134 | [[package]] 3135 | name = "starlette" 3136 | version = "0.46.1" 3137 | source = { registry = "https://pypi.org/simple" } 3138 | dependencies = [ 3139 | { name = "anyio" }, 3140 | ] 3141 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 3142 | wheels = [ 3143 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 3144 | ] 3145 | 3146 | [[package]] 3147 | name = "tomli" 3148 | version = "2.2.1" 3149 | source = { registry = "https://pypi.org/simple" } 3150 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 3151 | wheels = [ 3152 | { 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 }, 3153 | { 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 }, 3154 | { 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 }, 3155 | { 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 }, 3156 | { 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 }, 3157 | { 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 }, 3158 | { 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 }, 3159 | { 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 }, 3160 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 3161 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 3162 | { 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 }, 3163 | { 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 }, 3164 | { 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 }, 3165 | { 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 }, 3166 | { 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 }, 3167 | { 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 }, 3168 | { 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 }, 3169 | { 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 }, 3170 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 3171 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 3172 | { 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 }, 3173 | { 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 }, 3174 | { 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 }, 3175 | { 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 }, 3176 | { 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 }, 3177 | { 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 }, 3178 | { 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 }, 3179 | { 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 }, 3180 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 3181 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 3182 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 3183 | ] 3184 | 3185 | [[package]] 3186 | name = "typing-extensions" 3187 | version = "4.12.2" 3188 | source = { registry = "https://pypi.org/simple" } 3189 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 3190 | wheels = [ 3191 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 3192 | ] 3193 | 3194 | [[package]] 3195 | name = "uvicorn" 3196 | version = "0.34.0" 3197 | source = { registry = "https://pypi.org/simple" } 3198 | dependencies = [ 3199 | { name = "click" }, 3200 | { name = "h11" }, 3201 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 3202 | ] 3203 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 3204 | wheels = [ 3205 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 3206 | ] 3207 | </file> 3208 | 3209 | <file path="specs/pocket-pick-v1.md"> 3210 | # Pocket Pick - Your Personal Knowledge Base 3211 | 3212 | 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? 3213 | 3214 | 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. 3215 | 3216 | To implement this we'll... 3217 | 1. Build the key sqlite functionality 3218 | 2. Test the functionality with pytest 3219 | 3. Expose the functionality via MCP server. 3220 | 3221 | ## SQLITE Database Structure 3222 | 3223 | ``` 3224 | CREATE TABLE if not exists POCKET_PICK { 3225 | id: str, 3226 | created: datetime, 3227 | text: str, 3228 | tags: str[], 3229 | } 3230 | ``` 3231 | 3232 | ## Implementation Notes 3233 | - DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" - place in constants.py 3234 | - always force (auto update) tags to be lowercase, trim whitespace, and use dash instead of spaces or underscores. 3235 | - mcp comands will return whatever the command returns. 3236 | - mirror ai_docs/mcp-server-git-repomix-output.xml structure to understand how to setup the mcp server 3237 | - use ai_docs/paic-pkb-repomix-output.xml to get a rough understanding of what we're building. 3238 | - libraries should be 3239 | - click 3240 | - mcp 3241 | - pydantic 3242 | - pytest (dev dependency) 3243 | - sqlite3 (standard library) 3244 | - use `uv add <package>` to add libraries. 3245 | - we're using uv to manage the project. 3246 | - add mcp-server-pocket-pick = "mcp_server_pocket_pick:main" to the project.scripts section in pyproject.toml 3247 | 3248 | ## API 3249 | 3250 | ``` 3251 | pocket add <text> \ 3252 | --tags, t: str[] (optional) 3253 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3254 | 3255 | pocket find <text> \ 3256 | --mode: substr | fts | glob | regex | exact (optional) \ 3257 | --limit, -l: number = 5 \ 3258 | --info, -i: bool (show with metadata like id) \ 3259 | --tags, -t: str[] (optional) \ 3260 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3261 | 3262 | pocket list \ 3263 | --tags, -t: str[] (optional) \ 3264 | --limit, -l: number = 100 \ 3265 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3266 | 3267 | pocket list-tags \ 3268 | --limit, -l: number = 1000 \ 3269 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3270 | 3271 | pocket remove \ 3272 | --id, -i: str \ 3273 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3274 | 3275 | pocket get \ 3276 | --id, -i: str \ 3277 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3278 | 3279 | pocket backup <backup_absolute_path> \ 3280 | --db: str = DEFAULT_SQLITE_DATABASE_PATH 3281 | ``` 3282 | 3283 | ### Example API Calls (for find modes) 3284 | ``` 3285 | # basic sqlite substring search 3286 | pocket find "test" --mode substr 3287 | 3288 | # full text search 3289 | pocket find "test" --mode fts 3290 | 3291 | # glob search 3292 | pocket find "test*" --mode glob 3293 | 3294 | # regex search 3295 | pocket find "^start.*test.*$" --mode regex 3296 | 3297 | # exact search 3298 | pocket find "match exactly test" --mode exact 3299 | ``` 3300 | 3301 | ## Project Structure 3302 | - src/ 3303 | - mcp_server_pocket_pick/ 3304 | - __init__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml 3305 | - __main__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml 3306 | - server.py - MIRROR but use our functionality 3307 | - serve(sqlite_database: Path | None) -> None 3308 | - pass sqlite_database to every tool call (--db arg) 3309 | - modules/ 3310 | - __init__.py 3311 | - init_db.py 3312 | - data_types.py 3313 | - class AddCommand(BaseModel) {text: str, tags: list[str] = [], db_path: Path = DEFAULT_SQLITE_DATABASE_PATH} 3314 | - ... 3315 | - constants.py 3316 | - DEFAULT_SQLITE_DATABASE_PATH: Path = Path.home() / ".pocket_pick.db" 3317 | - functionality/ 3318 | - add.py 3319 | - find.py 3320 | - list.py 3321 | - list_tags.py 3322 | - remove.py 3323 | - get.py 3324 | - backup.py 3325 | - tests/ 3326 | - __init__.py 3327 | - test_init_db.py 3328 | - functionality/ 3329 | - test_add.py 3330 | - test_find.py 3331 | - test_list.py 3332 | - test_list_tags.py 3333 | - test_remove.py 3334 | - test_get.py 3335 | - test_backup.py 3336 | 3337 | 3338 | ## Validation (close the loop) 3339 | - use `uv run pytest` to validate the tests pass. 3340 | - use `uv run mcp-server-pocket-pick --help` to validate the mcp server works. 3341 | </file> 3342 | 3343 | <file path="src/mcp_server_pocket_pick/modules/functionality/find.py"> 3344 | import sqlite3 3345 | import json 3346 | from datetime import datetime 3347 | from typing import List 3348 | import logging 3349 | import re 3350 | from ..data_types import FindCommand, PocketItem 3351 | from ..init_db import init_db, normalize_tags 3352 | 3353 | logger = logging.getLogger(__name__) 3354 | 3355 | def find(command: FindCommand) -> List[PocketItem]: 3356 | """ 3357 | Find items in the pocket pick database matching the search criteria 3358 | 3359 | Args: 3360 | command: FindCommand with search parameters 3361 | 3362 | Returns: 3363 | List[PocketItem]: List of matching items 3364 | """ 3365 | # Normalize tags 3366 | normalized_tags = normalize_tags(command.tags) if command.tags else [] 3367 | 3368 | # Connect to database 3369 | db = init_db(command.db_path) 3370 | 3371 | try: 3372 | # Base query 3373 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 3374 | params = [] 3375 | where_clauses = [] 3376 | 3377 | # Apply search mode 3378 | if command.text: 3379 | if command.mode == "substr": 3380 | where_clauses.append("text LIKE ?") 3381 | params.append(f"%{command.text}%") 3382 | elif command.mode == "fts": 3383 | try: 3384 | # First, try using FTS5 virtual table 3385 | # Replace normal query with FTS query 3386 | query = """ 3387 | SELECT POCKET_PICK.id, POCKET_PICK.created, POCKET_PICK.text, POCKET_PICK.tags 3388 | FROM pocket_pick_fts 3389 | JOIN POCKET_PICK ON pocket_pick_fts.rowid = POCKET_PICK.rowid 3390 | """ 3391 | 3392 | # FTS5 query syntax 3393 | if command.mode == "fts": 3394 | # Check for different query formats 3395 | 3396 | # Direct quoted phrase - user already provided quotes for exact phrases 3397 | if command.text.startswith('"') and command.text.endswith('"'): 3398 | # User wants exact phrase matching (e.g., "word1 word2") 3399 | # Just use it directly - FTS5 understands quoted phrases 3400 | search_term = command.text 3401 | logger.debug(f"Using quoted phrase search: {search_term}") 3402 | 3403 | # Multi-word regular search 3404 | elif ' ' in command.text: 3405 | # Default: Match all terms independently (AND behavior) 3406 | search_term = command.text 3407 | 3408 | # Single word search 3409 | else: 3410 | search_term = command.text 3411 | else: 3412 | search_term = command.text 3413 | 3414 | # Using standard FTS5 query approach 3415 | 3416 | # Set up FTS5 query parameters 3417 | where_clauses = [f"pocket_pick_fts MATCH ?"] 3418 | params = [search_term] 3419 | 3420 | # FTS5 table doesn't have these columns, so we need to add tags filter separately 3421 | if normalized_tags: 3422 | tag_clauses = [] 3423 | for tag in normalized_tags: 3424 | tag_clauses.append("POCKET_PICK.tags LIKE ?") 3425 | params.append(f"%\"{tag}\"%") 3426 | 3427 | where_clauses.append(f"({' AND '.join(tag_clauses)})") 3428 | 3429 | # We'll handle the query execution in a special way 3430 | use_fts5 = True 3431 | except sqlite3.OperationalError: 3432 | # Fallback to basic LIKE-based search if FTS5 is not available 3433 | logger.warning("FTS5 not available, falling back to basic search") 3434 | use_fts5 = False 3435 | 3436 | # Standard fallback approach (original implementation) 3437 | search_words = command.text.split() 3438 | word_clauses = [] 3439 | for word in search_words: 3440 | word_clauses.append("text LIKE ?") 3441 | params.append(f"%{word}%") 3442 | where_clauses.append(f"({' AND '.join(word_clauses)})") 3443 | elif command.mode == "glob": 3444 | where_clauses.append("text GLOB ?") 3445 | params.append(command.text) 3446 | elif command.mode == "regex": 3447 | # We'll need to filter with regex after query 3448 | pass 3449 | elif command.mode == "exact": 3450 | where_clauses.append("text = ?") 3451 | params.append(command.text) 3452 | 3453 | # Apply tag filter if tags are specified 3454 | if normalized_tags: 3455 | # Find items that have all the specified tags 3456 | # We need to check if each tag exists in the JSON array 3457 | tag_clauses = [] 3458 | for tag in normalized_tags: 3459 | tag_clauses.append("tags LIKE ?") 3460 | # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets 3461 | params.append(f"%\"{tag}\"%") 3462 | 3463 | where_clauses.append(f"({' AND '.join(tag_clauses)})") 3464 | 3465 | # Handle query construction based on whether we're using FTS5 3466 | if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: 3467 | # For FTS5, we've already constructed the base query 3468 | if where_clauses: 3469 | query += f" WHERE {' AND '.join(where_clauses)}" 3470 | 3471 | # Special ordering for FTS5 to get the best matches first 3472 | query += f" ORDER BY rank, created DESC LIMIT {command.limit}" 3473 | 3474 | logger.debug(f"Using FTS5 query: {query}") 3475 | else: 3476 | # Standard query construction 3477 | if where_clauses: 3478 | query += f" WHERE {' AND '.join(where_clauses)}" 3479 | 3480 | # Apply limit 3481 | query += f" ORDER BY created DESC LIMIT {command.limit}" 3482 | 3483 | # Execute query 3484 | try: 3485 | cursor = db.execute(query, params) 3486 | except sqlite3.OperationalError as e: 3487 | # If the FTS5 query fails, fall back to the basic query 3488 | if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: 3489 | logger.warning(f"FTS5 query failed: {e}. Falling back to basic search.") 3490 | 3491 | # Reset to base query 3492 | query = "SELECT id, created, text, tags FROM POCKET_PICK" 3493 | params = [] 3494 | 3495 | # Standard fallback approach 3496 | if command.text: 3497 | search_words = command.text.split() 3498 | word_clauses = [] 3499 | for word in search_words: 3500 | word_clauses.append("text LIKE ?") 3501 | params.append(f"%{word}%") 3502 | query += f" WHERE ({' AND '.join(word_clauses)})" 3503 | 3504 | # Re-add tag filters if needed 3505 | if normalized_tags: 3506 | tag_clauses = [] 3507 | for tag in normalized_tags: 3508 | tag_clauses.append("tags LIKE ?") 3509 | params.append(f"%\"{tag}\"%") 3510 | 3511 | query += f" AND ({' AND '.join(tag_clauses)})" 3512 | 3513 | query += f" ORDER BY created DESC LIMIT {command.limit}" 3514 | cursor = db.execute(query, params) 3515 | else: 3516 | # If it's not an FTS5 issue, re-raise the exception 3517 | raise 3518 | 3519 | # Process results 3520 | results = [] 3521 | for row in cursor.fetchall(): 3522 | id, created_str, text, tags_json = row 3523 | 3524 | # Parse the created timestamp 3525 | created = datetime.fromisoformat(created_str) 3526 | 3527 | # Parse the tags JSON 3528 | tags = json.loads(tags_json) 3529 | 3530 | # Create item 3531 | item = PocketItem( 3532 | id=id, 3533 | created=created, 3534 | text=text, 3535 | tags=tags 3536 | ) 3537 | 3538 | # Apply regex filter if needed (we do this after the SQL query) 3539 | if command.mode == "regex" and command.text: 3540 | try: 3541 | pattern = re.compile(command.text, re.IGNORECASE) 3542 | if not pattern.search(text): 3543 | continue 3544 | except re.error: 3545 | logger.warning(f"Invalid regex pattern: {command.text}") 3546 | continue 3547 | 3548 | results.append(item) 3549 | 3550 | return results 3551 | except Exception as e: 3552 | logger.error(f"Error finding items: {e}") 3553 | raise 3554 | finally: 3555 | db.close() 3556 | </file> 3557 | 3558 | <file path="src/mcp_server_pocket_pick/modules/data_types.py"> 3559 | from pathlib import Path 3560 | from pydantic import BaseModel 3561 | from typing import List, Optional 3562 | from datetime import datetime 3563 | from .constants import DEFAULT_SQLITE_DATABASE_PATH 3564 | 3565 | 3566 | class AddCommand(BaseModel): 3567 | text: str 3568 | tags: List[str] = [] 3569 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3570 | 3571 | 3572 | class AddFileCommand(BaseModel): 3573 | file_path: str 3574 | tags: List[str] = [] 3575 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3576 | 3577 | 3578 | class FindCommand(BaseModel): 3579 | text: str 3580 | mode: str = "substr" # substr | fts | glob | regex | exact 3581 | limit: int = 5 3582 | info: bool = False 3583 | tags: List[str] = [] 3584 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3585 | 3586 | 3587 | class ListCommand(BaseModel): 3588 | tags: List[str] = [] 3589 | limit: int = 100 3590 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3591 | 3592 | 3593 | class ListTagsCommand(BaseModel): 3594 | limit: int = 1000 3595 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3596 | 3597 | 3598 | class RemoveCommand(BaseModel): 3599 | id: str 3600 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3601 | 3602 | 3603 | class GetCommand(BaseModel): 3604 | id: str 3605 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3606 | 3607 | 3608 | class BackupCommand(BaseModel): 3609 | backup_path: Path 3610 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3611 | 3612 | 3613 | class ToFileByIdCommand(BaseModel): 3614 | id: str 3615 | output_file_path_abs: Path 3616 | db_path: Path = DEFAULT_SQLITE_DATABASE_PATH 3617 | 3618 | 3619 | class PocketItem(BaseModel): 3620 | id: str 3621 | created: datetime 3622 | text: str 3623 | tags: List[str] 3624 | </file> 3625 | 3626 | <file path="src/mcp_server_pocket_pick/tests/functionality/test_find.py"> 3627 | import pytest 3628 | import tempfile 3629 | import os 3630 | from pathlib import Path 3631 | import json 3632 | import sqlite3 3633 | from datetime import datetime 3634 | from ...modules.data_types import AddCommand, FindCommand, PocketItem 3635 | from ...modules.functionality.add import add 3636 | from ...modules.functionality.find import find 3637 | from ...modules.init_db import init_db 3638 | 3639 | @pytest.fixture 3640 | def temp_db_path(): 3641 | # Create a temporary file path 3642 | fd, path = tempfile.mkstemp() 3643 | os.close(fd) 3644 | 3645 | # Return the path as a Path object 3646 | yield Path(path) 3647 | 3648 | # Clean up the temp file after test 3649 | if os.path.exists(path): 3650 | os.unlink(path) 3651 | 3652 | @pytest.fixture 3653 | def populated_db(temp_db_path): 3654 | # Create sample items 3655 | items = [ 3656 | {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, 3657 | {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, 3658 | {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, 3659 | {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, 3660 | {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} 3661 | ] 3662 | 3663 | # Add items to the database 3664 | for item in items: 3665 | command = AddCommand( 3666 | text=item["text"], 3667 | tags=item["tags"], 3668 | db_path=temp_db_path 3669 | ) 3670 | add(command) 3671 | 3672 | return temp_db_path 3673 | 3674 | def test_find_substr(populated_db): 3675 | # Search for "programming" substring 3676 | command = FindCommand( 3677 | text="programming", 3678 | mode="substr", 3679 | limit=10, 3680 | db_path=populated_db 3681 | ) 3682 | 3683 | results = find(command) 3684 | 3685 | # Should match "Python programming is fun" 3686 | assert len(results) == 1 3687 | assert "Python programming is fun" in [r.text for r in results] 3688 | 3689 | def test_find_fts(populated_db): 3690 | # Test basic FTS search with a single word 3691 | command = FindCommand( 3692 | text="SQL", 3693 | mode="fts", 3694 | limit=10, 3695 | db_path=populated_db 3696 | ) 3697 | 3698 | results = find(command) 3699 | 3700 | # Should match "SQL databases are powerful" 3701 | assert len(results) == 1 3702 | assert "SQL databases are powerful" in [r.text for r in results] 3703 | 3704 | def test_find_fts_phrase(populated_db): 3705 | # Test FTS with a phrase (multiple words in exact order) 3706 | command = FindCommand( 3707 | text="Regular expressions", 3708 | mode="fts", 3709 | limit=10, 3710 | db_path=populated_db 3711 | ) 3712 | 3713 | results = find(command) 3714 | 3715 | # Should match "Regular expressions can be complex" 3716 | assert len(results) == 1 3717 | assert "Regular expressions can be complex" in [r.text for r in results] 3718 | 3719 | def test_find_fts_multi_term(populated_db): 3720 | # Test FTS with multiple terms (not necessarily in order) 3721 | command = FindCommand( 3722 | text="programming fun", 3723 | mode="fts", 3724 | limit=10, 3725 | db_path=populated_db 3726 | ) 3727 | 3728 | results = find(command) 3729 | 3730 | # Should match items containing both "programming" and "fun" 3731 | assert len(results) > 0 3732 | 3733 | # Check that all results contain both "programming" AND "fun" 3734 | for result in results: 3735 | assert "programming" in result.text.lower() and "fun" in result.text.lower() 3736 | 3737 | def test_find_fts_with_tags(populated_db): 3738 | # Test FTS with tag filtering 3739 | command = FindCommand( 3740 | text="programming", 3741 | mode="fts", 3742 | tags=["fun"], # Only items tagged with "fun" 3743 | limit=10, 3744 | db_path=populated_db 3745 | ) 3746 | 3747 | results = find(command) 3748 | 3749 | # Should match items containing "programming" AND tagged with "fun" 3750 | assert len(results) == 1 3751 | assert "Python programming is fun" in [r.text for r in results] 3752 | 3753 | def test_find_fts_exact_phrase(populated_db): 3754 | """ 3755 | Test exact phrase matching functionality. 3756 | 3757 | This test is simplified to focus on the core functionality without relying 3758 | on specific matching patterns that might be hard to reproduce with FTS5. 3759 | """ 3760 | # First make sure we have a known item with a specific phrase 3761 | command = AddCommand( 3762 | text="This contains programming fun as a phrase", 3763 | tags=["test", "phrase"], 3764 | db_path=populated_db 3765 | ) 3766 | result1 = add(command) 3767 | 3768 | # Add an item with same words but in reverse order 3769 | command = AddCommand( 3770 | text="This has fun programming in reverse order", 3771 | tags=["test", "reverse"], 3772 | db_path=populated_db 3773 | ) 3774 | result2 = add(command) 3775 | 3776 | # Search using quoted exact phrase matching 3777 | command = FindCommand( 3778 | text='"programming fun"', # The quotes force exact phrase matching in FTS5 3779 | mode="fts", 3780 | limit=10, 3781 | db_path=populated_db 3782 | ) 3783 | 3784 | results = find(command) 3785 | 3786 | # Verify that our item with the exact phrase is found 3787 | # And the item with reversed words is not found 3788 | found_exact = "This contains programming fun as a phrase" in [r.text for r in results] 3789 | found_reverse = "This has fun programming in reverse order" in [r.text for r in results] 3790 | 3791 | assert found_exact, "Should find item with exact phrase" 3792 | assert not found_reverse, "Should not find item with reverse word order" 3793 | 3794 | def test_find_glob(populated_db): 3795 | # Search for text starting with "Test" 3796 | command = FindCommand( 3797 | text="Test*", 3798 | mode="glob", 3799 | limit=10, 3800 | db_path=populated_db 3801 | ) 3802 | 3803 | results = find(command) 3804 | 3805 | # Should match "Testing code is important" 3806 | assert len(results) == 1 3807 | assert "Testing code is important" in [r.text for r in results] 3808 | 3809 | def test_find_regex(populated_db): 3810 | # Search for text containing "regular" (case insensitive) 3811 | command = FindCommand( 3812 | text=".*regular.*", 3813 | mode="regex", 3814 | limit=10, 3815 | db_path=populated_db 3816 | ) 3817 | 3818 | results = find(command) 3819 | 3820 | # Should match "Regular expressions can be complex" 3821 | assert len(results) == 1 3822 | assert "Regular expressions can be complex" in [r.text for r in results] 3823 | 3824 | def test_find_exact(populated_db): 3825 | # Search for exact match 3826 | command = FindCommand( 3827 | text="Learning new technologies is exciting", 3828 | mode="exact", 3829 | limit=10, 3830 | db_path=populated_db 3831 | ) 3832 | 3833 | results = find(command) 3834 | 3835 | # Should match exactly one item 3836 | assert len(results) == 1 3837 | assert results[0].text == "Learning new technologies is exciting" 3838 | 3839 | def test_find_with_tags(populated_db): 3840 | # Search for items with specific tags 3841 | command = FindCommand( 3842 | text="", # No text search 3843 | tags=["fun"], 3844 | limit=10, 3845 | db_path=populated_db 3846 | ) 3847 | 3848 | results = find(command) 3849 | 3850 | # Should match items with the "fun" tag 3851 | assert len(results) == 2 3852 | assert "Python programming is fun" in [r.text for r in results] 3853 | assert "Learning new technologies is exciting" in [r.text for r in results] 3854 | 3855 | def test_find_with_text_and_tags(populated_db): 3856 | # Search for items with specific text and tags 3857 | command = FindCommand( 3858 | text="programming", 3859 | mode="substr", 3860 | tags=["fun"], 3861 | limit=10, 3862 | db_path=populated_db 3863 | ) 3864 | 3865 | results = find(command) 3866 | 3867 | # Should match items with "programming" text and "fun" tag 3868 | assert len(results) == 1 3869 | assert "Python programming is fun" in [r.text for r in results] 3870 | 3871 | def test_find_limit(populated_db): 3872 | # Search with limit 3873 | command = FindCommand( 3874 | text="", # Match all 3875 | limit=2, 3876 | db_path=populated_db 3877 | ) 3878 | 3879 | results = find(command) 3880 | 3881 | # Should only return 2 items (due to limit) 3882 | assert len(results) == 2 3883 | </file> 3884 | 3885 | <file path="src/mcp_server_pocket_pick/__init__.py"> 3886 | import click 3887 | from pathlib import Path 3888 | import logging 3889 | import sys 3890 | from .server import serve 3891 | 3892 | @click.command() 3893 | @click.option("--database", "-d", type=Path, help="SQLite database path (default: ~/.pocket_pick.db)") 3894 | @click.option("-v", "--verbose", count=True) 3895 | def main(database: Path | None, verbose: bool) -> None: 3896 | """Pocket Pick - Your Personal Knowledge Base""" 3897 | import asyncio 3898 | 3899 | logging_level = logging.WARN 3900 | if verbose == 1: 3901 | logging_level = logging.INFO 3902 | elif verbose >= 2: 3903 | logging_level = logging.DEBUG 3904 | 3905 | logging.basicConfig(level=logging_level, stream=sys.stderr) 3906 | asyncio.run(serve(database)) 3907 | 3908 | if __name__ == "__main__": 3909 | main() 3910 | </file> 3911 | 3912 | <file path="src/mcp_server_pocket_pick/modules/init_db.py"> 3913 | import sqlite3 3914 | from pathlib import Path 3915 | import logging 3916 | 3917 | logger = logging.getLogger(__name__) 3918 | 3919 | def init_db(db_path: Path) -> sqlite3.Connection: 3920 | """Initialize SQLite database with POCKET_PICK table""" 3921 | # Ensure parent directory exists 3922 | db_path.parent.mkdir(parents=True, exist_ok=True) 3923 | 3924 | logger.info(f"Initializing database at {db_path}") 3925 | # Ensure the directory exists 3926 | if not db_path.parent.exists(): 3927 | logger.info(f"Creating directory {db_path.parent}") 3928 | db_path.parent.mkdir(parents=True, exist_ok=True) 3929 | 3930 | db = sqlite3.connect(str(db_path)) 3931 | 3932 | # Enable foreign keys 3933 | db.execute("PRAGMA foreign_keys = ON") 3934 | 3935 | # Create the POCKET_PICK table 3936 | db.execute(""" 3937 | CREATE TABLE IF NOT EXISTS POCKET_PICK ( 3938 | id TEXT PRIMARY KEY, 3939 | created TIMESTAMP NOT NULL, 3940 | text TEXT NOT NULL, 3941 | tags TEXT NOT NULL 3942 | ) 3943 | """) 3944 | 3945 | # Create indexes for efficient searching 3946 | db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_created ON POCKET_PICK(created)") 3947 | db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_text ON POCKET_PICK(text)") 3948 | 3949 | # Create FTS5 virtual table for full-text search 3950 | try: 3951 | db.execute(""" 3952 | CREATE VIRTUAL TABLE IF NOT EXISTS pocket_pick_fts USING fts5( 3953 | text, 3954 | content='POCKET_PICK', 3955 | content_rowid='rowid' 3956 | ) 3957 | """) 3958 | 3959 | # Create triggers to keep FTS index up to date 3960 | db.execute(""" 3961 | CREATE TRIGGER IF NOT EXISTS pocket_pick_ai AFTER INSERT ON POCKET_PICK 3962 | BEGIN 3963 | INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); 3964 | END 3965 | """) 3966 | 3967 | db.execute(""" 3968 | CREATE TRIGGER IF NOT EXISTS pocket_pick_ad AFTER DELETE ON POCKET_PICK 3969 | BEGIN 3970 | INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); 3971 | END 3972 | """) 3973 | 3974 | db.execute(""" 3975 | CREATE TRIGGER IF NOT EXISTS pocket_pick_au AFTER UPDATE ON POCKET_PICK 3976 | BEGIN 3977 | INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); 3978 | INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); 3979 | END 3980 | """) 3981 | 3982 | # Rebuild FTS index if needed (for existing data) 3983 | db.execute(""" 3984 | INSERT OR IGNORE INTO pocket_pick_fts(rowid, text) 3985 | SELECT rowid, text FROM POCKET_PICK 3986 | """) 3987 | 3988 | except sqlite3.OperationalError as e: 3989 | # If FTS5 is not available, log a warning but continue 3990 | logger.warning(f"FTS5 extension not available: {e}. Full-text search will fallback to basic search.") 3991 | 3992 | # Commit changes 3993 | db.commit() 3994 | 3995 | return db 3996 | 3997 | def normalize_tag(tag: str) -> str: 3998 | """ 3999 | Normalize tags: 4000 | - lowercase 4001 | - trim whitespace 4002 | - replace spaces and underscores with dashes 4003 | """ 4004 | tag = tag.lower().strip() 4005 | return tag.replace(' ', '-').replace('_', '-') 4006 | 4007 | def normalize_tags(tags: list[str]) -> list[str]: 4008 | """Apply normalization to a list of tags""" 4009 | return [normalize_tag(tag) for tag in tags] 4010 | </file> 4011 | 4012 | <file path="src/mcp_server_pocket_pick/server.py"> 4013 | import logging 4014 | from pathlib import Path 4015 | from typing import Sequence, List 4016 | from mcp.server import Server 4017 | from mcp.server.session import ServerSession 4018 | from mcp.server.stdio import stdio_server 4019 | from mcp.types import ( 4020 | ClientCapabilities, 4021 | TextContent, 4022 | Tool, 4023 | ListRootsResult, 4024 | RootsCapability, 4025 | ) 4026 | from enum import Enum 4027 | from pydantic import BaseModel 4028 | 4029 | from .modules.data_types import ( 4030 | AddCommand, 4031 | AddFileCommand, 4032 | FindCommand, 4033 | ListCommand, 4034 | ListTagsCommand, 4035 | RemoveCommand, 4036 | GetCommand, 4037 | BackupCommand, 4038 | ToFileByIdCommand, 4039 | ) 4040 | from .modules.functionality.add import add 4041 | from .modules.functionality.add_file import add_file 4042 | from .modules.functionality.find import find 4043 | from .modules.functionality.list import list_items 4044 | from .modules.functionality.list_tags import list_tags 4045 | from .modules.functionality.remove import remove 4046 | from .modules.functionality.get import get 4047 | from .modules.functionality.backup import backup 4048 | from .modules.functionality.to_file_by_id import to_file_by_id 4049 | from .modules.constants import DEFAULT_SQLITE_DATABASE_PATH 4050 | 4051 | logger = logging.getLogger(__name__) 4052 | 4053 | class PocketAdd(BaseModel): 4054 | text: str 4055 | tags: List[str] = [] 4056 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4057 | 4058 | class PocketAddFile(BaseModel): 4059 | file_path: str 4060 | tags: List[str] = [] 4061 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4062 | 4063 | class PocketFind(BaseModel): 4064 | text: str 4065 | mode: str = "substr" 4066 | limit: int = 5 4067 | info: bool = False 4068 | tags: List[str] = [] 4069 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4070 | 4071 | class PocketList(BaseModel): 4072 | tags: List[str] = [] 4073 | limit: int = 100 4074 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4075 | 4076 | class PocketListTags(BaseModel): 4077 | limit: int = 1000 4078 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4079 | 4080 | class PocketRemove(BaseModel): 4081 | id: str 4082 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4083 | 4084 | class PocketGet(BaseModel): 4085 | id: str 4086 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4087 | 4088 | class PocketBackup(BaseModel): 4089 | backup_path: str 4090 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4091 | 4092 | class PocketToFileById(BaseModel): 4093 | id: str 4094 | output_file_path_abs: str 4095 | db: str = str(DEFAULT_SQLITE_DATABASE_PATH) 4096 | 4097 | class PocketTools(str, Enum): 4098 | ADD = "pocket_add" 4099 | ADD_FILE = "pocket_add_file" 4100 | FIND = "pocket_find" 4101 | LIST = "pocket_list" 4102 | LIST_TAGS = "pocket_list_tags" 4103 | REMOVE = "pocket_remove" 4104 | GET = "pocket_get" 4105 | BACKUP = "pocket_backup" 4106 | TO_FILE_BY_ID = "pocket_to_file_by_id" 4107 | 4108 | async def serve(sqlite_database: Path | None = None) -> None: 4109 | logger.info(f"Starting Pocket Pick MCP server") 4110 | 4111 | # Determine which database path to use 4112 | db_path = sqlite_database if sqlite_database is not None else DEFAULT_SQLITE_DATABASE_PATH 4113 | logger.info(f"Using database at {db_path}") 4114 | 4115 | # Initialize the database at startup to ensure it exists 4116 | from .modules.init_db import init_db 4117 | connection = init_db(db_path) 4118 | connection.close() 4119 | logger.info(f"Database initialized at {db_path}") 4120 | 4121 | server = Server("pocket-pick") 4122 | 4123 | @server.list_tools() 4124 | async def list_tools() -> list[Tool]: 4125 | return [ 4126 | Tool( 4127 | name=PocketTools.ADD, 4128 | description="Add a new item to your pocket pick database", 4129 | inputSchema=PocketAdd.schema(), 4130 | ), 4131 | Tool( 4132 | name=PocketTools.ADD_FILE, 4133 | description="Add a new item to your pocket pick database from a file", 4134 | inputSchema=PocketAddFile.schema(), 4135 | ), 4136 | Tool( 4137 | name=PocketTools.FIND, 4138 | description="Find items in your pocket pick database by text and tags", 4139 | inputSchema=PocketFind.schema(), 4140 | ), 4141 | Tool( 4142 | name=PocketTools.LIST, 4143 | description="List items in your pocket pick database, optionally filtered by tags", 4144 | inputSchema=PocketList.schema(), 4145 | ), 4146 | Tool( 4147 | name=PocketTools.LIST_TAGS, 4148 | description="List all tags in your pocket pick database with their counts", 4149 | inputSchema=PocketListTags.schema(), 4150 | ), 4151 | Tool( 4152 | name=PocketTools.REMOVE, 4153 | description="Remove an item from your pocket pick database by ID", 4154 | inputSchema=PocketRemove.schema(), 4155 | ), 4156 | Tool( 4157 | name=PocketTools.GET, 4158 | description="Get an item from your pocket pick database by ID", 4159 | inputSchema=PocketGet.schema(), 4160 | ), 4161 | Tool( 4162 | name=PocketTools.BACKUP, 4163 | description="Backup your pocket pick database to a specified location", 4164 | inputSchema=PocketBackup.schema(), 4165 | ), 4166 | Tool( 4167 | name=PocketTools.TO_FILE_BY_ID, 4168 | description="Write a pocket pick item's content to a file by its ID (requires absolute file path)", 4169 | inputSchema=PocketToFileById.schema(), 4170 | ), 4171 | ] 4172 | 4173 | @server.call_tool() 4174 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 4175 | # Override db_path if provided via command line 4176 | if sqlite_database is not None: 4177 | arguments["db"] = str(sqlite_database) 4178 | elif "db" not in arguments: 4179 | # Use default if not specified 4180 | arguments["db"] = str(DEFAULT_SQLITE_DATABASE_PATH) 4181 | 4182 | db_path = Path(arguments["db"]) 4183 | 4184 | # Ensure the database exists and is initialized for every command 4185 | from .modules.init_db import init_db 4186 | connection = init_db(db_path) 4187 | connection.close() 4188 | 4189 | match name: 4190 | case PocketTools.ADD: 4191 | command = AddCommand( 4192 | text=arguments["text"], 4193 | tags=arguments.get("tags", []), 4194 | db_path=db_path 4195 | ) 4196 | result = add(command) 4197 | return [TextContent( 4198 | type="text", 4199 | text=f"Added item with ID: {result.id}\nText: {result.text}\nTags: {', '.join(result.tags)}" 4200 | )] 4201 | 4202 | case PocketTools.ADD_FILE: 4203 | command = AddFileCommand( 4204 | file_path=arguments["file_path"], 4205 | tags=arguments.get("tags", []), 4206 | db_path=db_path 4207 | ) 4208 | result = add_file(command) 4209 | return [TextContent( 4210 | type="text", 4211 | text=f"Added file content with ID: {result.id}\nFrom file: {arguments['file_path']}\nTags: {', '.join(result.tags)}" 4212 | )] 4213 | 4214 | case PocketTools.FIND: 4215 | command = FindCommand( 4216 | text=arguments["text"], 4217 | mode=arguments.get("mode", "substr"), 4218 | limit=arguments.get("limit", 5), 4219 | info=arguments.get("info", False), 4220 | tags=arguments.get("tags", []), 4221 | db_path=db_path 4222 | ) 4223 | results = find(command) 4224 | 4225 | if not results: 4226 | return [TextContent( 4227 | type="text", 4228 | text="No items found matching your search criteria." 4229 | )] 4230 | 4231 | output = [] 4232 | for item in results: 4233 | if command.info: 4234 | output.append(f"ID: {item.id}") 4235 | output.append(f"Created: {item.created.isoformat()}") 4236 | output.append(f"Tags: {', '.join(item.tags)}") 4237 | output.append(f"Text: {item.text}") 4238 | output.append("") 4239 | else: 4240 | output.append(item.text) 4241 | output.append("") 4242 | 4243 | return [TextContent( 4244 | type="text", 4245 | text="\n".join(output).strip() 4246 | )] 4247 | 4248 | case PocketTools.LIST: 4249 | command = ListCommand( 4250 | tags=arguments.get("tags", []), 4251 | limit=arguments.get("limit", 100), 4252 | db_path=db_path 4253 | ) 4254 | results = list_items(command) 4255 | 4256 | if not results: 4257 | return [TextContent( 4258 | type="text", 4259 | text="No items found." 4260 | )] 4261 | 4262 | output = [] 4263 | for item in results: 4264 | output.append(f"ID: {item.id}") 4265 | output.append(f"Created: {item.created.isoformat()}") 4266 | output.append(f"Tags: {', '.join(item.tags)}") 4267 | output.append(f"Text: {item.text}") 4268 | output.append("") 4269 | 4270 | return [TextContent( 4271 | type="text", 4272 | text="\n".join(output).strip() 4273 | )] 4274 | 4275 | case PocketTools.LIST_TAGS: 4276 | command = ListTagsCommand( 4277 | limit=arguments.get("limit", 1000), 4278 | db_path=db_path 4279 | ) 4280 | results = list_tags(command) 4281 | 4282 | if not results: 4283 | return [TextContent( 4284 | type="text", 4285 | text="No tags found." 4286 | )] 4287 | 4288 | output = ["Tags:"] 4289 | for item in results: 4290 | output.append(f"{item['tag']} ({item['count']})") 4291 | 4292 | return [TextContent( 4293 | type="text", 4294 | text="\n".join(output) 4295 | )] 4296 | 4297 | case PocketTools.REMOVE: 4298 | command = RemoveCommand( 4299 | id=arguments["id"], 4300 | db_path=db_path 4301 | ) 4302 | result = remove(command) 4303 | 4304 | if result: 4305 | return [TextContent( 4306 | type="text", 4307 | text=f"Item {command.id} removed successfully." 4308 | )] 4309 | else: 4310 | return [TextContent( 4311 | type="text", 4312 | text=f"Item {command.id} not found." 4313 | )] 4314 | 4315 | case PocketTools.GET: 4316 | command = GetCommand( 4317 | id=arguments["id"], 4318 | db_path=db_path 4319 | ) 4320 | result = get(command) 4321 | 4322 | if result: 4323 | return [TextContent( 4324 | type="text", 4325 | text=f"ID: {result.id}\nCreated: {result.created.isoformat()}\nTags: {', '.join(result.tags)}\nText: {result.text}" 4326 | )] 4327 | else: 4328 | return [TextContent( 4329 | type="text", 4330 | text=f"Item {command.id} not found." 4331 | )] 4332 | 4333 | case PocketTools.BACKUP: 4334 | command = BackupCommand( 4335 | backup_path=Path(arguments["backup_path"]), 4336 | db_path=db_path 4337 | ) 4338 | result = backup(command) 4339 | 4340 | if result: 4341 | return [TextContent( 4342 | type="text", 4343 | text=f"Database backed up successfully to {command.backup_path}" 4344 | )] 4345 | else: 4346 | return [TextContent( 4347 | type="text", 4348 | text=f"Failed to backup database to {command.backup_path}" 4349 | )] 4350 | 4351 | case PocketTools.TO_FILE_BY_ID: 4352 | command = ToFileByIdCommand( 4353 | id=arguments["id"], 4354 | output_file_path_abs=Path(arguments["output_file_path_abs"]), 4355 | db_path=db_path 4356 | ) 4357 | result = to_file_by_id(command) 4358 | 4359 | if result: 4360 | return [TextContent( 4361 | type="text", 4362 | text=f"Content written successfully to {command.output_file_path_abs}" 4363 | )] 4364 | else: 4365 | return [TextContent( 4366 | type="text", 4367 | text=f"Failed to write content to {command.output_file_path_abs}" 4368 | )] 4369 | 4370 | case _: 4371 | raise ValueError(f"Unknown tool: {name}") 4372 | 4373 | options = server.create_initialization_options() 4374 | async with stdio_server() as (read_stream, write_stream): 4375 | await server.run(read_stream, write_stream, options, raise_exceptions=True) 4376 | </file> 4377 | 4378 | <file path=".gitignore"> 4379 | # Byte-compiled / optimized / DLL files 4380 | __pycache__/ 4381 | *.py[cod] 4382 | *$py.class 4383 | 4384 | # C extensions 4385 | *.so 4386 | 4387 | # Distribution / packaging 4388 | .Python 4389 | build/ 4390 | develop-eggs/ 4391 | dist/ 4392 | downloads/ 4393 | eggs/ 4394 | .eggs/ 4395 | lib/ 4396 | lib64/ 4397 | parts/ 4398 | sdist/ 4399 | var/ 4400 | wheels/ 4401 | share/python-wheels/ 4402 | *.egg-info/ 4403 | .installed.cfg 4404 | *.egg 4405 | MANIFEST 4406 | 4407 | # PyInstaller 4408 | # Usually these files are written by a python script from a template 4409 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 4410 | *.manifest 4411 | *.spec 4412 | 4413 | # Installer logs 4414 | pip-log.txt 4415 | pip-delete-this-directory.txt 4416 | 4417 | # Unit test / coverage reports 4418 | htmlcov/ 4419 | .tox/ 4420 | .nox/ 4421 | .coverage 4422 | .coverage.* 4423 | .cache 4424 | nosetests.xml 4425 | coverage.xml 4426 | *.cover 4427 | *.py,cover 4428 | .hypothesis/ 4429 | .pytest_cache/ 4430 | cover/ 4431 | 4432 | # Environments 4433 | .env 4434 | .venv 4435 | env/ 4436 | venv/ 4437 | ENV/ 4438 | env.bak/ 4439 | venv.bak/ 4440 | 4441 | # mypy 4442 | .mypy_cache/ 4443 | .dmypy.json 4444 | dmypy.json 4445 | 4446 | # Editors 4447 | .vscode/ 4448 | .idea/ 4449 | *.swp 4450 | *.swo 4451 | 4452 | 4453 | ai_docs/paic-pkb-repomix-output.xml 4454 | </file> 4455 | 4456 | <file path="pyproject.toml"> 4457 | [project] 4458 | name = "mcp-server-pocket-pick" 4459 | version = "0.1.0" 4460 | description = "Your Personal Knowledge Base with MCP" 4461 | readme = "README.md" 4462 | authors = [ 4463 | { name = "IndyDevDan", email = "[email protected]" } 4464 | ] 4465 | requires-python = ">=3.10" 4466 | dependencies = [ 4467 | "click>=8.1.7", 4468 | "mcp>=1.0.0", 4469 | "pydantic>=2.0.0", 4470 | ] 4471 | 4472 | [project.scripts] 4473 | mcp-server-pocket-pick = "mcp_server_pocket_pick:main" 4474 | 4475 | [build-system] 4476 | requires = ["hatchling"] 4477 | build-backend = "hatchling.build" 4478 | 4479 | [tool.uv] 4480 | dev-dependencies = ["pytest>=8.0.0"] 4481 | </file> 4482 | 4483 | <file path="README.md"> 4484 | # Pocket Pick (MCP Server) 4485 | > See how we used AI Coding, Claude Code, and MCP to build this tool on the [@IndyDevDan youtube channel](https://youtu.be/d-SyGA0Avtw). 4486 | 4487 | 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? 4488 | 4489 | 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. 4490 | 4491 | <img src="./images/pocket-pick.png" alt="Pocket Pick" style="max-width: 600px;"> 4492 | 4493 | ## Features 4494 | 4495 | - **Personal Knowledge Base**: Store code snippets, information, and ideas 4496 | - **Tag-Based Organization**: Add tags to categorize and filter your knowledge 4497 | - **Flexible Search**: Find content using substring, full-text, glob, regex, or exact matching 4498 | - **MCP Integration**: Seamlessly works with Claude and other MCP-compatible AI assistants 4499 | - **SQLite Backend**: Fast, reliable, and portable database storage 4500 | - **Command-Line Interface**: Easy to use from the terminal 4501 | 4502 | ## Installation 4503 | 4504 | Install [uv](https://docs.astral.sh/uv/getting-started/installation/) 4505 | 4506 | ```bash 4507 | # Clone the repository 4508 | git clone https://github.com/indydevdan/pocket-pick.git 4509 | cd pocket-pick 4510 | 4511 | # Install dependencies 4512 | uv sync 4513 | ``` 4514 | 4515 | Usage from JSON format 4516 | 4517 | Default Database for Claude Code 4518 | 4519 | ```json 4520 | { 4521 | "command": "uv", 4522 | "args": ["--directory", ".", "run", "mcp-server-pocket-pick"] 4523 | } 4524 | ``` 4525 | 4526 | Custom Database for Claude Code 4527 | 4528 | ```json 4529 | { 4530 | "command": "uv", 4531 | "args": ["--directory", ".", "run", "mcp-server-pocket-pick", "--database", "./database.db"] 4532 | } 4533 | ``` 4534 | 4535 | ## Usage with Claude Code 4536 | 4537 | ```bash 4538 | # Add the pocket-pick server to Claude Code (if you're in the directory) 4539 | claude mcp add pocket-pick -- \ 4540 | uv --directory . \ 4541 | run mcp-server-pocket-pick 4542 | 4543 | # Add the pocket-pick server to Claude Code 4544 | claude mcp add pocket-pick -- \ 4545 | uv --directory /path/to/pocket-pick-codebase \ 4546 | run mcp-server-pocket-pick 4547 | 4548 | # With custom database location 4549 | claude mcp add pocket-pick -- \ 4550 | uv --directory /path/to/pocket-pick-codebase \ 4551 | run mcp-server-pocket-pick --database ./database.db 4552 | 4553 | # List existing MCP servers - Validate that the server is running 4554 | claude mcp list 4555 | 4556 | # Start claude code 4557 | claude 4558 | ``` 4559 | 4560 | ## Pocket Pick MCP Tools 4561 | 4562 | The following MCP tools are available in Pocket Pick: 4563 | 4564 | | Tool | Description | 4565 | | -------------------- | -------------------------------------------- | 4566 | | `pocket_add` | Add a new item to your knowledge base | 4567 | | `pocket_add_file` | Add a file's content to your knowledge base | 4568 | | `pocket_find` | Find items by text and/or tags | 4569 | | `pocket_list` | List all items, optionally filtered by tags | 4570 | | `pocket_list_tags` | List all tags with their counts | 4571 | | `pocket_remove` | Remove an item by ID | 4572 | | `pocket_get` | Get a specific item by ID | 4573 | | `pocket_backup` | Backup the database | 4574 | | `pocket_to_file_by_id` | Write an item's content to a file by its ID (requires absolute path) | 4575 | 4576 | ## Using with Claude 4577 | 4578 | After setting up Pocket Pick as an MCP server for Claude Code, you can use it your conversations: 4579 | 4580 | ### Adding Items 4581 | 4582 | Add items directly 4583 | 4584 | ```bash 4585 | Add "claude mcp list" as a pocket pick item. tags: mcp, claude, code 4586 | ``` 4587 | 4588 | Add items from clipboard 4589 | 4590 | ```bash 4591 | pbpaste and create a pocket pick item with the following tags: python, algorithm, fibonacci 4592 | ``` 4593 | 4594 | Add items from a file 4595 | 4596 | ```bash 4597 | Add the contents of ~/Documents/code-snippets/fibonacci.py to pocket pick with tags: python, algorithm, fibonacci 4598 | ``` 4599 | 4600 | ### Listing Items 4601 | List all items or tags: 4602 | 4603 | ``` 4604 | list all my pocket picks 4605 | ``` 4606 | 4607 | ### Finding Items 4608 | 4609 | Search for items in your knowledge base with tags 4610 | 4611 | ``` 4612 | List pocket pick items with python and mcp tags 4613 | ``` 4614 | 4615 | Search for text with specific content 4616 | 4617 | ``` 4618 | pocket pick find "python" 4619 | ``` 4620 | 4621 | ### Get or Remove Items 4622 | 4623 | Get or remove specific items: 4624 | 4625 | ``` 4626 | get the pocket pick item with ID 1234-5678-90ab-cdef 4627 | remove the pocket pick item with ID 1234-5678-90ab-cdef 4628 | ``` 4629 | 4630 | ### Export to File 4631 | 4632 | 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: 4633 | 4634 | ``` 4635 | export the pocket pick item with ID 1234-5678-90ab-cdef to /Users/username/Documents/exported-snippet.py 4636 | ``` 4637 | 4638 | The tool requires an absolute file path and will automatically create any necessary parent directories if they don't exist. 4639 | 4640 | ### Backup 4641 | 4642 | ``` 4643 | backup the pocket pick database to ~/Documents/pocket-pick-backup.db 4644 | ``` 4645 | 4646 | ## Search Modes 4647 | 4648 | Pocket Pick supports various search modes: 4649 | 4650 | - **substr**: (Default) Simple substring matching 4651 | - **fts**: Full-text search with powerful capabilities: 4652 | - Regular word search: Matches all words in any order (e.g., "python programming" finds entries with both words) 4653 | - Exact phrase search: Use quotes for exact phrase matching (e.g., `"python programming"` only finds entries with that exact phrase) 4654 | - **glob**: SQLite glob pattern matching (e.g., "test*" matches entries starting with "test") 4655 | - **regex**: Regular expression matching 4656 | - **exact**: Exact string matching 4657 | 4658 | Example find commands: 4659 | 4660 | ``` 4661 | Find items containing "pyt" using substring matching 4662 | Find items containing "def fibonacci" using full text search 4663 | Find items containing "test*" using glob pattern matching 4664 | Find items containing "^start.*test.*$" using regular expression matching 4665 | Find items containing "match exactly test" using exact string matching 4666 | ``` 4667 | 4668 | ## Database Structure 4669 | 4670 | Pocket Pick uses a simple SQLite database with the following schema: 4671 | 4672 | ```sql 4673 | CREATE TABLE POCKET_PICK ( 4674 | id TEXT PRIMARY KEY, -- UUID identifier 4675 | created TIMESTAMP NOT NULL, -- Creation timestamp 4676 | text TEXT NOT NULL, -- Item content 4677 | tags TEXT NOT NULL -- JSON array of tags 4678 | ) 4679 | ``` 4680 | 4681 | The database file is located at `~/.pocket_pick.db` by default. 4682 | 4683 | ## Development 4684 | 4685 | ### Running Tests 4686 | 4687 | ```bash 4688 | # Run all tests 4689 | uv run pytest 4690 | 4691 | # Run with verbose output 4692 | uv run pytest -v 4693 | ``` 4694 | 4695 | ### Running the Server Directly 4696 | 4697 | ```bash 4698 | # Start the MCP server 4699 | uv run mcp-server-pocket-pick 4700 | 4701 | # With verbose logging 4702 | uv run mcp-server-pocket-pick -v 4703 | 4704 | # With custom database location 4705 | uv run mcp-server-pocket-pick --database ./database.db 4706 | ``` 4707 | 4708 | ## Other Useful MCP Servers 4709 | 4710 | ### Fetch 4711 | 4712 | ```bash 4713 | claude mcp add http-fetch -- uvx mcp-server-fetch 4714 | ``` 4715 | 4716 | --- 4717 | 4718 | 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) 4719 | </file> 4720 | 4721 | </files> 4722 | ```