#
tokens: 85047/50000 1/88 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 4/4FirstPrevNextLast