This is page 4 of 4. Use http://codebase.md/disler/just-prompt?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: -------------------------------------------------------------------------------- ``` This file is a merged representation of the entire codebase, combined into a single document by Repomix. <file_summary> This section contains a summary of this file. <purpose> This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. </purpose> <file_format> The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Repository files, each consisting of: - File path as an attribute - Full contents of the file </file_format> <usage_guidelines> - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. </usage_guidelines> <notes> - Some files may have been excluded based on .gitignore rules and Repomix's configuration - 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 - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded - Files are sorted by Git change count (files with more changes are at the bottom) </notes> <additional_info> </additional_info> </file_summary> <directory_structure> ai_docs/ mcp-server-git-repomix-output.xml specs/ pocket-pick-v1.md src/ mcp_server_pocket_pick/ modules/ functionality/ __init__.py add_file.py add.py backup.py find.py get.py list_tags.py list.py remove.py to_file_by_id.py __init__.py constants.py data_types.py init_db.py tests/ functionality/ __init__.py test_add_file.py test_add.py test_backup.py test_find.py test_list_tags.py test_list.py test_remove_get.py test_to_file_by_id.py __init__.py test_init_db.py __init__.py __main__.py server.py .gitignore .python-version pyproject.toml README.md uv.lock </directory_structure> <files> This section contains the contents of the repository's files. <file path="ai_docs/mcp-server-git-repomix-output.xml"> This file is a merged representation of the entire codebase, combined into a single document by Repomix. <file_summary> This section contains a summary of this file. <purpose> This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. </purpose> <file_format> The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Repository files, each consisting of: - File path as an attribute - Full contents of the file </file_format> <usage_guidelines> - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. </usage_guidelines> <notes> - Some files may have been excluded based on .gitignore rules and Repomix's configuration - 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 - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded </notes> <additional_info> </additional_info> </file_summary> <directory_structure> src/ mcp_server_git/ __init__.py __main__.py server.py tests/ test_server.py .gitignore .python-version Dockerfile LICENSE pyproject.toml README.md uv.lock </directory_structure> <files> This section contains the contents of the repository's files. <file path="src/mcp_server_git/__init__.py"> import click from pathlib import Path import logging import sys from .server import serve @click.command() @click.option("--repository", "-r", type=Path, help="Git repository path") @click.option("-v", "--verbose", count=True) def main(repository: Path | None, verbose: bool) -> None: """MCP Git Server - Git functionality for MCP""" import asyncio logging_level = logging.WARN if verbose == 1: logging_level = logging.INFO elif verbose >= 2: logging_level = logging.DEBUG logging.basicConfig(level=logging_level, stream=sys.stderr) asyncio.run(serve(repository)) if __name__ == "__main__": main() </file> <file path="src/mcp_server_git/__main__.py"> # __main__.py from mcp_server_git import main main() </file> <file path="src/mcp_server_git/server.py"> import logging from pathlib import Path from typing import Sequence from mcp.server import Server from mcp.server.session import ServerSession from mcp.server.stdio import stdio_server from mcp.types import ( ClientCapabilities, TextContent, Tool, ListRootsResult, RootsCapability, ) from enum import Enum import git from pydantic import BaseModel class GitStatus(BaseModel): repo_path: str class GitDiffUnstaged(BaseModel): repo_path: str class GitDiffStaged(BaseModel): repo_path: str class GitDiff(BaseModel): repo_path: str target: str class GitCommit(BaseModel): repo_path: str message: str class GitAdd(BaseModel): repo_path: str files: list[str] class GitReset(BaseModel): repo_path: str class GitLog(BaseModel): repo_path: str max_count: int = 10 class GitCreateBranch(BaseModel): repo_path: str branch_name: str base_branch: str | None = None class GitCheckout(BaseModel): repo_path: str branch_name: str class GitShow(BaseModel): repo_path: str revision: str class GitInit(BaseModel): repo_path: str class GitTools(str, Enum): STATUS = "git_status" DIFF_UNSTAGED = "git_diff_unstaged" DIFF_STAGED = "git_diff_staged" DIFF = "git_diff" COMMIT = "git_commit" ADD = "git_add" RESET = "git_reset" LOG = "git_log" CREATE_BRANCH = "git_create_branch" CHECKOUT = "git_checkout" SHOW = "git_show" INIT = "git_init" def git_status(repo: git.Repo) -> str: return repo.git.status() def git_diff_unstaged(repo: git.Repo) -> str: return repo.git.diff() def git_diff_staged(repo: git.Repo) -> str: return repo.git.diff("--cached") def git_diff(repo: git.Repo, target: str) -> str: return repo.git.diff(target) def git_commit(repo: git.Repo, message: str) -> str: commit = repo.index.commit(message) return f"Changes committed successfully with hash {commit.hexsha}" def git_add(repo: git.Repo, files: list[str]) -> str: repo.index.add(files) return "Files staged successfully" def git_reset(repo: git.Repo) -> str: repo.index.reset() return "All staged changes reset" def git_log(repo: git.Repo, max_count: int = 10) -> list[str]: commits = list(repo.iter_commits(max_count=max_count)) log = [] for commit in commits: log.append( f"Commit: {commit.hexsha}\n" f"Author: {commit.author}\n" f"Date: {commit.authored_datetime}\n" f"Message: {commit.message}\n" ) return log def git_create_branch(repo: git.Repo, branch_name: str, base_branch: str | None = None) -> str: if base_branch: base = repo.refs[base_branch] else: base = repo.active_branch repo.create_head(branch_name, base) return f"Created branch '{branch_name}' from '{base.name}'" def git_checkout(repo: git.Repo, branch_name: str) -> str: repo.git.checkout(branch_name) return f"Switched to branch '{branch_name}'" def git_init(repo_path: str) -> str: try: repo = git.Repo.init(path=repo_path, mkdir=True) return f"Initialized empty Git repository in {repo.git_dir}" except Exception as e: return f"Error initializing repository: {str(e)}" def git_show(repo: git.Repo, revision: str) -> str: commit = repo.commit(revision) output = [ f"Commit: {commit.hexsha}\n" f"Author: {commit.author}\n" f"Date: {commit.authored_datetime}\n" f"Message: {commit.message}\n" ] if commit.parents: parent = commit.parents[0] diff = parent.diff(commit, create_patch=True) else: diff = commit.diff(git.NULL_TREE, create_patch=True) for d in diff: output.append(f"\n--- {d.a_path}\n+++ {d.b_path}\n") output.append(d.diff.decode('utf-8')) return "".join(output) async def serve(repository: Path | None) -> None: logger = logging.getLogger(__name__) if repository is not None: try: git.Repo(repository) logger.info(f"Using repository at {repository}") except git.InvalidGitRepositoryError: logger.error(f"{repository} is not a valid Git repository") return server = Server("mcp-git") @server.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name=GitTools.STATUS, description="Shows the working tree status", inputSchema=GitStatus.schema(), ), Tool( name=GitTools.DIFF_UNSTAGED, description="Shows changes in the working directory that are not yet staged", inputSchema=GitDiffUnstaged.schema(), ), Tool( name=GitTools.DIFF_STAGED, description="Shows changes that are staged for commit", inputSchema=GitDiffStaged.schema(), ), Tool( name=GitTools.DIFF, description="Shows differences between branches or commits", inputSchema=GitDiff.schema(), ), Tool( name=GitTools.COMMIT, description="Records changes to the repository", inputSchema=GitCommit.schema(), ), Tool( name=GitTools.ADD, description="Adds file contents to the staging area", inputSchema=GitAdd.schema(), ), Tool( name=GitTools.RESET, description="Unstages all staged changes", inputSchema=GitReset.schema(), ), Tool( name=GitTools.LOG, description="Shows the commit logs", inputSchema=GitLog.schema(), ), Tool( name=GitTools.CREATE_BRANCH, description="Creates a new branch from an optional base branch", inputSchema=GitCreateBranch.schema(), ), Tool( name=GitTools.CHECKOUT, description="Switches branches", inputSchema=GitCheckout.schema(), ), Tool( name=GitTools.SHOW, description="Shows the contents of a commit", inputSchema=GitShow.schema(), ), Tool( name=GitTools.INIT, description="Initialize a new Git repository", inputSchema=GitInit.schema(), ) ] async def list_repos() -> Sequence[str]: async def by_roots() -> Sequence[str]: if not isinstance(server.request_context.session, ServerSession): raise TypeError("server.request_context.session must be a ServerSession") if not server.request_context.session.check_client_capability( ClientCapabilities(roots=RootsCapability()) ): return [] roots_result: ListRootsResult = await server.request_context.session.list_roots() logger.debug(f"Roots result: {roots_result}") repo_paths = [] for root in roots_result.roots: path = root.uri.path try: git.Repo(path) repo_paths.append(str(path)) except git.InvalidGitRepositoryError: pass return repo_paths def by_commandline() -> Sequence[str]: return [str(repository)] if repository is not None else [] cmd_repos = by_commandline() root_repos = await by_roots() return [*root_repos, *cmd_repos] @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: repo_path = Path(arguments["repo_path"]) # Handle git init separately since it doesn't require an existing repo if name == GitTools.INIT: result = git_init(str(repo_path)) return [TextContent( type="text", text=result )] # For all other commands, we need an existing repo repo = git.Repo(repo_path) match name: case GitTools.STATUS: status = git_status(repo) return [TextContent( type="text", text=f"Repository status:\n{status}" )] case GitTools.DIFF_UNSTAGED: diff = git_diff_unstaged(repo) return [TextContent( type="text", text=f"Unstaged changes:\n{diff}" )] case GitTools.DIFF_STAGED: diff = git_diff_staged(repo) return [TextContent( type="text", text=f"Staged changes:\n{diff}" )] case GitTools.DIFF: diff = git_diff(repo, arguments["target"]) return [TextContent( type="text", text=f"Diff with {arguments['target']}:\n{diff}" )] case GitTools.COMMIT: result = git_commit(repo, arguments["message"]) return [TextContent( type="text", text=result )] case GitTools.ADD: result = git_add(repo, arguments["files"]) return [TextContent( type="text", text=result )] case GitTools.RESET: result = git_reset(repo) return [TextContent( type="text", text=result )] case GitTools.LOG: log = git_log(repo, arguments.get("max_count", 10)) return [TextContent( type="text", text="Commit history:\n" + "\n".join(log) )] case GitTools.CREATE_BRANCH: result = git_create_branch( repo, arguments["branch_name"], arguments.get("base_branch") ) return [TextContent( type="text", text=result )] case GitTools.CHECKOUT: result = git_checkout(repo, arguments["branch_name"]) return [TextContent( type="text", text=result )] case GitTools.SHOW: result = git_show(repo, arguments["revision"]) return [TextContent( type="text", text=result )] case _: raise ValueError(f"Unknown tool: {name}") options = server.create_initialization_options() async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, options, raise_exceptions=True) </file> <file path="tests/test_server.py"> import pytest from pathlib import Path import git from mcp_server_git.server import git_checkout import shutil @pytest.fixture def test_repository(tmp_path: Path): repo_path = tmp_path / "temp_test_repo" test_repo = git.Repo.init(repo_path) Path(repo_path / "test.txt").write_text("test") test_repo.index.add(["test.txt"]) test_repo.index.commit("initial commit") yield test_repo shutil.rmtree(repo_path) def test_git_checkout_existing_branch(test_repository): test_repository.git.branch("test-branch") result = git_checkout(test_repository, "test-branch") assert "Switched to branch 'test-branch'" in result assert test_repository.active_branch.name == "test-branch" def test_git_checkout_nonexistent_branch(test_repository): with pytest.raises(git.GitCommandError): git_checkout(test_repository, "nonexistent-branch") </file> <file path=".gitignore"> __pycache__ .venv </file> <file path=".python-version"> 3.10 </file> <file path="Dockerfile"> # Use a Python image with uv pre-installed FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv # Install the project into `/app` WORKDIR /app # Enable bytecode compilation ENV UV_COMPILE_BYTECODE=1 # Copy from the cache instead of linking since it's a mounted volume ENV UV_LINK_MODE=copy # Install the project's dependencies using the lockfile and settings RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev --no-editable # Then, add the rest of the project source code and install it # Installing separately from its dependencies allows optimal layer caching ADD . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-editable FROM python:3.12-slim-bookworm RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=uv /root/.local /root/.local COPY --from=uv --chown=app:app /app/.venv /app/.venv # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" # when running the container, add --db-path and a bind mount to the host's db file ENTRYPOINT ["mcp-server-git"] </file> <file path="LICENSE"> Copyright (c) 2024 Anthropic, PBC. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. </file> <file path="pyproject.toml"> [project] name = "mcp-server-git" version = "0.6.2" description = "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs" readme = "README.md" requires-python = ">=3.10" authors = [{ name = "Anthropic, PBC." }] maintainers = [{ name = "David Soria Parra", email = "[email protected]" }] keywords = ["git", "mcp", "llm", "automation"] license = { text = "MIT" } classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", ] dependencies = [ "click>=8.1.7", "gitpython>=3.1.43", "mcp>=1.0.0", "pydantic>=2.0.0", ] [project.scripts] mcp-server-git = "mcp_server_git:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv] dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3", "pytest>=8.0.0"] [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" python_classes = "Test*" python_functions = "test_*" </file> <file path="README.md"> # mcp-server-git: A git MCP server ## Overview 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. 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. ### Tools 1. `git_status` - Shows the working tree status - Input: - `repo_path` (string): Path to Git repository - Returns: Current status of working directory as text output 2. `git_diff_unstaged` - Shows changes in working directory not yet staged - Input: - `repo_path` (string): Path to Git repository - Returns: Diff output of unstaged changes 3. `git_diff_staged` - Shows changes that are staged for commit - Input: - `repo_path` (string): Path to Git repository - Returns: Diff output of staged changes 4. `git_diff` - Shows differences between branches or commits - Inputs: - `repo_path` (string): Path to Git repository - `target` (string): Target branch or commit to compare with - Returns: Diff output comparing current state with target 5. `git_commit` - Records changes to the repository - Inputs: - `repo_path` (string): Path to Git repository - `message` (string): Commit message - Returns: Confirmation with new commit hash 6. `git_add` - Adds file contents to the staging area - Inputs: - `repo_path` (string): Path to Git repository - `files` (string[]): Array of file paths to stage - Returns: Confirmation of staged files 7. `git_reset` - Unstages all staged changes - Input: - `repo_path` (string): Path to Git repository - Returns: Confirmation of reset operation 8. `git_log` - Shows the commit logs - Inputs: - `repo_path` (string): Path to Git repository - `max_count` (number, optional): Maximum number of commits to show (default: 10) - Returns: Array of commit entries with hash, author, date, and message 9. `git_create_branch` - Creates a new branch - Inputs: - `repo_path` (string): Path to Git repository - `branch_name` (string): Name of the new branch - `start_point` (string, optional): Starting point for the new branch - Returns: Confirmation of branch creation 10. `git_checkout` - Switches branches - Inputs: - `repo_path` (string): Path to Git repository - `branch_name` (string): Name of branch to checkout - Returns: Confirmation of branch switch 11. `git_show` - Shows the contents of a commit - Inputs: - `repo_path` (string): Path to Git repository - `revision` (string): The revision (commit hash, branch name, tag) to show - Returns: Contents of the specified commit 12. `git_init` - Initializes a Git repository - Inputs: - `repo_path` (string): Path to directory to initialize git repo - Returns: Confirmation of repository initialization ## Installation ### Using uv (recommended) When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-git*. ### Using PIP Alternatively you can install `mcp-server-git` via pip: ``` pip install mcp-server-git ``` After installation, you can run it as a script using: ``` python -m mcp_server_git ``` ## Configuration ### Usage with Claude Desktop Add this to your `claude_desktop_config.json`: <details> <summary>Using uvx</summary> ```json "mcpServers": { "git": { "command": "uvx", "args": ["mcp-server-git", "--repository", "path/to/git/repo"] } } ``` </details> <details> <summary>Using docker</summary> * Note: replace '/Users/username' with the a path that you want to be accessible by this tool ```json "mcpServers": { "git": { "command": "docker", "args": ["run", "--rm", "-i", "--mount", "type=bind,src=/Users/username,dst=/Users/username", "mcp/git"] } } ``` </details> <details> <summary>Using pip installation</summary> ```json "mcpServers": { "git": { "command": "python", "args": ["-m", "mcp_server_git", "--repository", "path/to/git/repo"] } } ``` </details> ### Usage with [Zed](https://github.com/zed-industries/zed) Add to your Zed settings.json: <details> <summary>Using uvx</summary> ```json "context_servers": [ "mcp-server-git": { "command": { "path": "uvx", "args": ["mcp-server-git"] } } ], ``` </details> <details> <summary>Using pip installation</summary> ```json "context_servers": { "mcp-server-git": { "command": { "path": "python", "args": ["-m", "mcp_server_git"] } } }, ``` </details> ## Debugging You can use the MCP inspector to debug the server. For uvx installations: ``` npx @modelcontextprotocol/inspector uvx mcp-server-git ``` Or if you've installed the package in a specific directory or are developing on it: ``` cd path/to/servers/src/git npx @modelcontextprotocol/inspector uv run mcp-server-git ``` Running `tail -n 20 -f ~/Library/Logs/Claude/mcp*.log` will show the logs from the server and may help you debug any issues. ## Development If you are doing local development, there are two ways to test your changes: 1. Run the MCP inspector to test your changes. See [Debugging](#debugging) for run instructions. 2. Test using the Claude desktop app. Add the following to your `claude_desktop_config.json`: ### Docker ```json { "mcpServers": { "git": { "command": "docker", "args": [ "run", "--rm", "-i", "--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop", "--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro", "--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt", "mcp/git" ] } } } ``` ### UVX ```json { "mcpServers": { "git": { "command": "uv", "args": [ "--directory", "/<path to mcp-servers>/mcp-servers/src/git", "run", "mcp-server-git" ] } } ``` ## Build Docker build: ```bash cd src/git docker build -t mcp/git . ``` ## License 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. </file> <file path="uv.lock"> version = 1 requires-python = ">=3.10" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anyio" version = "4.6.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] [[package]] name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "gitdb" version = "4.0.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, ] [[package]] name = "gitpython" version = "3.1.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "httpcore" version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] [[package]] name = "httpx" version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, { name = "sniffio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } wheels = [ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "mcp" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "pydantic" }, { name = "sse-starlette" }, { name = "starlette" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 } wheels = [ { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 }, ] [[package]] name = "mcp-server-git" version = "0.6.2" source = { editable = "." } dependencies = [ { name = "click" }, { name = "gitpython" }, { name = "mcp" }, { name = "pydantic" }, ] [package.dev-dependencies] dev = [ { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, ] [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "gitpython", specifier = ">=3.1.43" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "pydantic", specifier = ">=2.0.0" }, ] [package.metadata.requires-dev] dev = [ { name = "pyright", specifier = ">=1.1.389" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "ruff", specifier = ">=0.7.3" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pydantic" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", size = 783717 } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e", size = 455329 }, ] [[package]] name = "pydantic-core" version = "2.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } wheels = [ { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, ] [[package]] name = "pyright" version = "1.1.389" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 }, ] [[package]] name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] name = "ruff" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, ] [[package]] name = "smmap" version = "5.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sse-starlette" version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, { name = "uvicorn" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 } wheels = [ { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 }, ] [[package]] name = "starlette" version = "0.41.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "uvicorn" version = "0.32.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } wheels = [ { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, ] </file> </files> </file> <file path="src/mcp_server_pocket_pick/modules/functionality/__init__.py"> # Functionality module initialization </file> <file path="src/mcp_server_pocket_pick/modules/functionality/add_file.py"> import sqlite3 import uuid import json from datetime import datetime from pathlib import Path import logging from ..data_types import AddFileCommand, PocketItem from ..init_db import init_db, normalize_tags logger = logging.getLogger(__name__) def add_file(command: AddFileCommand) -> PocketItem: """ Add a new item to the pocket pick database from a file Args: command: AddFileCommand with file_path, tags and db_path Returns: PocketItem: The newly created item """ # Read the file content try: file_path = Path(command.file_path) if not file_path.exists(): raise FileNotFoundError(f"File not found: {file_path}") with open(file_path, 'r', encoding='utf-8') as f: text = f.read() except Exception as e: logger.error(f"Error reading file {command.file_path}: {e}") raise # Normalize tags normalized_tags = normalize_tags(command.tags) # Generate a unique ID item_id = str(uuid.uuid4()) # Get current timestamp timestamp = datetime.now() # Connect to database db = init_db(command.db_path) try: # Serialize tags to JSON tags_json = json.dumps(normalized_tags) # Insert item db.execute( "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", (item_id, timestamp.isoformat(), text, tags_json) ) # Commit transaction db.commit() # Return created item return PocketItem( id=item_id, created=timestamp, text=text, tags=normalized_tags ) except Exception as e: logger.error(f"Error adding item from file: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/add.py"> import sqlite3 import uuid import json from datetime import datetime from pathlib import Path import logging from ..data_types import AddCommand, PocketItem from ..init_db import init_db, normalize_tags logger = logging.getLogger(__name__) def add(command: AddCommand) -> PocketItem: """ Add a new item to the pocket pick database Args: command: AddCommand with text, tags and db_path Returns: PocketItem: The newly created item """ # Normalize tags normalized_tags = normalize_tags(command.tags) # Generate a unique ID item_id = str(uuid.uuid4()) # Get current timestamp timestamp = datetime.now() # Connect to database db = init_db(command.db_path) try: # Serialize tags to JSON tags_json = json.dumps(normalized_tags) # Insert item db.execute( "INSERT INTO POCKET_PICK (id, created, text, tags) VALUES (?, ?, ?, ?)", (item_id, timestamp.isoformat(), command.text, tags_json) ) # Commit transaction db.commit() # Return created item return PocketItem( id=item_id, created=timestamp, text=command.text, tags=normalized_tags ) except Exception as e: logger.error(f"Error adding item: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/backup.py"> import sqlite3 import shutil import logging from ..data_types import BackupCommand from ..init_db import init_db logger = logging.getLogger(__name__) def backup(command: BackupCommand) -> bool: """ Backup the pocket pick database to a specified location Args: command: BackupCommand with backup destination path Returns: bool: True if backup was successful, False otherwise """ # Make sure source DB exists by initializing it if needed db = init_db(command.db_path) db.close() try: # Create parent directories if they don't exist command.backup_path.parent.mkdir(parents=True, exist_ok=True) # Copy the database file to the backup location shutil.copy2(command.db_path, command.backup_path) # Verify the backup file exists if command.backup_path.exists(): logger.info(f"Backup created successfully at {command.backup_path}") return True else: logger.error(f"Backup file not found at {command.backup_path}") return False except Exception as e: logger.error(f"Error creating backup: {e}") return False </file> <file path="src/mcp_server_pocket_pick/modules/functionality/get.py"> import sqlite3 import json from datetime import datetime import logging from typing import Optional from ..data_types import GetCommand, PocketItem from ..init_db import init_db logger = logging.getLogger(__name__) def get(command: GetCommand) -> Optional[PocketItem]: """ Get an item from the pocket pick database by ID Args: command: GetCommand with item ID Returns: Optional[PocketItem]: The item if found, None otherwise """ # Connect to database db = init_db(command.db_path) try: # Query for item with given ID cursor = db.execute( "SELECT id, created, text, tags FROM POCKET_PICK WHERE id = ?", (command.id,) ) # Fetch the row row = cursor.fetchone() # If no row was found, return None if row is None: return None # Process the row id, created_str, text, tags_json = row # Parse the created timestamp created = datetime.fromisoformat(created_str) # Parse the tags JSON tags = json.loads(tags_json) # Create and return the item return PocketItem( id=id, created=created, text=text, tags=tags ) except Exception as e: logger.error(f"Error getting item {command.id}: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/list_tags.py"> import sqlite3 import json from typing import List, Dict import logging from ..data_types import ListTagsCommand from ..init_db import init_db logger = logging.getLogger(__name__) def list_tags(command: ListTagsCommand) -> List[Dict[str, int]]: """ List all tags in the pocket pick database with their counts Args: command: ListTagsCommand with limit Returns: List[Dict[str, int]]: List of dicts with tag name and count """ # Connect to database db = init_db(command.db_path) try: # Get all tags with their counts cursor = db.execute("SELECT tags FROM POCKET_PICK") # Process results to count tags tag_counts = {} for (tags_json,) in cursor.fetchall(): tags = json.loads(tags_json) for tag in tags: if tag in tag_counts: tag_counts[tag] += 1 else: tag_counts[tag] = 1 # Sort by count (descending) and then alphabetically sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0])) # Apply limit and format result result = [{"tag": tag, "count": count} for tag, count in sorted_tags[:command.limit]] return result except Exception as e: logger.error(f"Error listing tags: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/list.py"> import sqlite3 import json from datetime import datetime from typing import List import logging from ..data_types import ListCommand, PocketItem from ..init_db import init_db, normalize_tags logger = logging.getLogger(__name__) def list_items(command: ListCommand) -> List[PocketItem]: """ List items in the pocket pick database, optionally filtered by tags Args: command: ListCommand with optional tag filters and limit Returns: List[PocketItem]: List of matching items """ # Normalize tags normalized_tags = normalize_tags(command.tags) if command.tags else [] # Connect to database db = init_db(command.db_path) try: # Base query query = "SELECT id, created, text, tags FROM POCKET_PICK" params = [] # Apply tag filter if tags are specified if normalized_tags: # We need to check if each tag exists in the JSON array tag_clauses = [] for tag in normalized_tags: tag_clauses.append("tags LIKE ?") # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets params.append(f"%\"{tag}\"%") query += f" WHERE {' AND '.join(tag_clauses)}" # Apply order and limit query += f" ORDER BY created DESC LIMIT {command.limit}" # Execute query cursor = db.execute(query, params) # Process results results = [] for row in cursor.fetchall(): id, created_str, text, tags_json = row # Parse the created timestamp created = datetime.fromisoformat(created_str) # Parse the tags JSON tags = json.loads(tags_json) # Create item item = PocketItem( id=id, created=created, text=text, tags=tags ) results.append(item) return results except Exception as e: logger.error(f"Error listing items: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/remove.py"> import sqlite3 import logging from ..data_types import RemoveCommand from ..init_db import init_db logger = logging.getLogger(__name__) def remove(command: RemoveCommand) -> bool: """ Remove an item from the pocket pick database by ID Args: command: RemoveCommand with item ID Returns: bool: True if an item was removed, False if no matching item was found """ # Connect to database db = init_db(command.db_path) try: # Delete item with given ID cursor = db.execute("DELETE FROM POCKET_PICK WHERE id = ?", (command.id,)) # Commit the transaction db.commit() # Check if any row was affected return cursor.rowcount > 0 except Exception as e: logger.error(f"Error removing item {command.id}: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/functionality/to_file_by_id.py"> from pathlib import Path import logging import os from ..data_types import ToFileByIdCommand, PocketItem from .get import get from .get import GetCommand logger = logging.getLogger(__name__) def to_file_by_id(command: ToFileByIdCommand) -> bool: """ Write pocket pick content with given ID to the specified file Args: command: ToFileByIdCommand with id, output_file_path and db_path Returns: bool: True if successful, False otherwise """ try: # First get the item from the database get_command = GetCommand( id=command.id, db_path=command.db_path ) item = get(get_command) if not item: logger.error(f"Item with ID {command.id} not found") return False # Ensure parent directory exists output_path = Path(command.output_file_path_abs) output_path.parent.mkdir(parents=True, exist_ok=True) # Write content to file with open(output_path, 'w', encoding='utf-8') as f: f.write(item.text) return True except Exception as e: logger.error(f"Error writing to file {command.output_file_path_abs}: {e}") return False </file> <file path="src/mcp_server_pocket_pick/modules/__init__.py"> # Module initialization </file> <file path="src/mcp_server_pocket_pick/modules/constants.py"> from pathlib import Path DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" </file> <file path="src/mcp_server_pocket_pick/tests/functionality/__init__.py"> # Functionality tests package initialization </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_add_file.py"> import pytest import tempfile import os from pathlib import Path import json import sqlite3 from ...modules.data_types import AddFileCommand, PocketItem from ...modules.functionality.add_file import add_file @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def temp_file_with_content(): # Create a temporary file with content fd, path = tempfile.mkstemp() with os.fdopen(fd, 'w') as file: file.write("This is test content from a file") # Return the path as a string yield path # Clean up the temp file after test if os.path.exists(path): os.unlink(path) def test_add_file_simple(temp_db_path, temp_file_with_content): # Create a command to add a file content command = AddFileCommand( file_path=temp_file_with_content, tags=["test", "file"], db_path=temp_db_path ) # Add the item result = add_file(command) # Verify result is a PocketItem assert isinstance(result, PocketItem) assert result.text == "This is test content from a file" assert result.tags == ["test", "file"] assert result.id is not None # Verify item was added to the database db = sqlite3.connect(temp_db_path) cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") row = cursor.fetchone() assert row is not None assert row[0] == result.id assert row[1] == "This is test content from a file" # Verify tags were stored as JSON stored_tags = json.loads(row[2]) assert stored_tags == ["test", "file"] # Verify no more rows exist assert cursor.fetchone() is None db.close() def test_add_file_with_tag_normalization(temp_db_path, temp_file_with_content): # Create a command with tags that need normalization command = AddFileCommand( file_path=temp_file_with_content, tags=["FILE", "with space", "under_score"], db_path=temp_db_path ) # Add the item result = add_file(command) # Verify tags were normalized assert result.tags == ["file", "with-space", "under-score"] # Verify in database db = sqlite3.connect(temp_db_path) cursor = db.execute("SELECT tags FROM POCKET_PICK") row = cursor.fetchone() stored_tags = json.loads(row[0]) assert stored_tags == ["file", "with-space", "under-score"] db.close() def test_add_file_nonexistent(temp_db_path): # Create a command with a nonexistent file command = AddFileCommand( file_path="/nonexistent/file/path.txt", tags=["test"], db_path=temp_db_path ) # Expect FileNotFoundError when adding with pytest.raises(FileNotFoundError): add_file(command) </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_add.py"> import pytest import tempfile import os from pathlib import Path import json import sqlite3 from ...modules.data_types import AddCommand, PocketItem from ...modules.functionality.add import add @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) def test_add_simple(temp_db_path): # Create a command to add a simple item command = AddCommand( text="This is a test item", tags=["test", "example"], db_path=temp_db_path ) # Add the item result = add(command) # Verify result is a PocketItem assert isinstance(result, PocketItem) assert result.text == "This is a test item" assert result.tags == ["test", "example"] assert result.id is not None # Verify item was added to the database db = sqlite3.connect(temp_db_path) cursor = db.execute("SELECT id, text, tags FROM POCKET_PICK") row = cursor.fetchone() assert row is not None assert row[0] == result.id assert row[1] == "This is a test item" # Verify tags were stored as JSON stored_tags = json.loads(row[2]) assert stored_tags == ["test", "example"] # Verify no more rows exist assert cursor.fetchone() is None db.close() def test_add_with_tag_normalization(temp_db_path): # Create a command with tags that need normalization command = AddCommand( text="Item with tags to normalize", tags=["TAG", "with space", "under_score"], db_path=temp_db_path ) # Add the item result = add(command) # Verify tags were normalized assert result.tags == ["tag", "with-space", "under-score"] # Verify in database db = sqlite3.connect(temp_db_path) cursor = db.execute("SELECT tags FROM POCKET_PICK") row = cursor.fetchone() stored_tags = json.loads(row[0]) assert stored_tags == ["tag", "with-space", "under-score"] db.close() </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_backup.py"> import pytest import tempfile import os import sqlite3 from pathlib import Path from ...modules.data_types import AddCommand, BackupCommand from ...modules.functionality.add import add from ...modules.functionality.backup import backup @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def temp_backup_path(): # Create a temporary file path for backup fd, path = tempfile.mkstemp() os.close(fd) os.unlink(path) # Remove the file so backup can create it # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def populated_db(temp_db_path): # Add a test item to the database command = AddCommand( text="Test item for backup", tags=["test", "backup"], db_path=temp_db_path ) add(command) return temp_db_path def test_backup_success(populated_db, temp_backup_path): # Backup the database command = BackupCommand( backup_path=temp_backup_path, db_path=populated_db ) result = backup(command) # Should return True indicating success assert result is True # Verify backup file exists assert temp_backup_path.exists() # Verify backup contains the same data as original original_db = sqlite3.connect(populated_db) original_cursor = original_db.execute("SELECT id, text, tags FROM POCKET_PICK") original_row = original_cursor.fetchone() original_db.close() backup_db = sqlite3.connect(temp_backup_path) backup_cursor = backup_db.execute("SELECT id, text, tags FROM POCKET_PICK") backup_row = backup_cursor.fetchone() backup_db.close() assert backup_row is not None assert backup_row[0] == original_row[0] # ID assert backup_row[1] == original_row[1] # text assert backup_row[2] == original_row[2] # tags def test_backup_nested_directory_creation(populated_db): # Create a backup path in a nested directory that doesn't exist with tempfile.TemporaryDirectory() as temp_dir: nested_dir = Path(temp_dir) / "nested" / "dirs" backup_path = nested_dir / "backup.db" # Backup the database command = BackupCommand( backup_path=backup_path, db_path=populated_db ) result = backup(command) # Should return True indicating success assert result is True # Verify backup file exists assert backup_path.exists() def test_backup_from_nonexistent_db(temp_db_path, temp_backup_path): # Try to backup from a nonexistent database # (temp_db_path fixture exists but is empty) command = BackupCommand( backup_path=temp_backup_path, db_path=temp_db_path ) result = backup(command) # Should return True indicating success (empty database created and backed up) assert result is True assert temp_backup_path.exists() </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_list_tags.py"> import pytest import tempfile import os from pathlib import Path from ...modules.data_types import AddCommand, ListTagsCommand from ...modules.functionality.add import add from ...modules.functionality.list_tags import list_tags @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def populated_db(temp_db_path): # Create sample items items = [ {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} ] # Add items to the database for item in items: command = AddCommand( text=item["text"], tags=item["tags"], db_path=temp_db_path ) add(command) return temp_db_path def test_list_tags_all(populated_db): # List all tags command = ListTagsCommand( db_path=populated_db ) results = list_tags(command) # Verify all expected tags are present tags = [result["tag"] for result in results] expected_tags = [ "programming", # Count: 4 "fun", # Count: 2 "python", # Count: 1 "sql", # Count: 1 "database", # Count: 1 "testing", # Count: 1 "code", # Count: 1 "regex", # Count: 1 "advanced", # Count: 1 "learning", # Count: 1 "technology" # Count: 1 ] for expected in expected_tags: assert expected in tags # Verify the most common tag is first (sorted by count) assert results[0]["tag"] == "programming" assert results[0]["count"] == 4 # Verify second most common tag assert results[1]["tag"] == "fun" assert results[1]["count"] == 2 def test_list_tags_limit(populated_db): # List tags with limit command = ListTagsCommand( limit=3, db_path=populated_db ) results = list_tags(command) # Should only return 3 tags assert len(results) == 3 # Verify the top 3 tags in order by count # (with ties broken alphabetically) assert results[0]["tag"] == "programming" assert results[1]["tag"] == "fun" # The third item should be one of the single-count tags assert results[2]["count"] == 1 def test_list_tags_empty_db(temp_db_path): # List tags in empty database command = ListTagsCommand( db_path=temp_db_path ) results = list_tags(command) # Should return empty list assert len(results) == 0 </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_list.py"> import pytest import tempfile import os from pathlib import Path from ...modules.data_types import AddCommand, ListCommand from ...modules.functionality.add import add from ...modules.functionality.list import list_items @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def populated_db(temp_db_path): # Create sample items items = [ {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} ] # Add items to the database for item in items: command = AddCommand( text=item["text"], tags=item["tags"], db_path=temp_db_path ) add(command) return temp_db_path def test_list_all(populated_db): # List all items command = ListCommand( limit=10, db_path=populated_db ) results = list_items(command) # Should return all 5 items assert len(results) == 5 # Check that all expected texts are present texts = [result.text for result in results] expected_texts = [ "Python programming is fun", "SQL databases are powerful", "Testing code is important", "Regular expressions can be complex", "Learning new technologies is exciting" ] for expected in expected_texts: assert expected in texts def test_list_with_tags(populated_db): # List items with specific tag command = ListCommand( tags=["programming"], limit=10, db_path=populated_db ) results = list_items(command) # Should return items with the "programming" tag (4 items) assert len(results) == 4 # Verify the correct items are returned texts = [result.text for result in results] expected_texts = [ "Python programming is fun", "SQL databases are powerful", "Testing code is important", "Regular expressions can be complex" ] for expected in expected_texts: assert expected in texts def test_list_with_multiple_tags(populated_db): # List items with multiple tags command = ListCommand( tags=["programming", "fun"], limit=10, db_path=populated_db ) results = list_items(command) # Should return items with both "programming" and "fun" tags (1 item) assert len(results) == 1 assert results[0].text == "Python programming is fun" def test_list_limit(populated_db): # List with limit command = ListCommand( limit=2, db_path=populated_db ) results = list_items(command) # Should only return 2 items assert len(results) == 2 </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_remove_get.py"> import pytest import tempfile import os from pathlib import Path import sqlite3 from ...modules.data_types import AddCommand, RemoveCommand, GetCommand from ...modules.functionality.add import add from ...modules.functionality.remove import remove from ...modules.functionality.get import get @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def item_id(temp_db_path): # Add a test item and return its ID command = AddCommand( text="Test item for get and remove", tags=["test", "example"], db_path=temp_db_path ) result = add(command) return result.id, temp_db_path def test_get_item(item_id): id, db_path = item_id # Get the item by ID command = GetCommand( id=id, db_path=db_path ) result = get(command) # Verify item properties assert result is not None assert result.id == id assert result.text == "Test item for get and remove" assert set(result.tags) == set(["test", "example"]) def test_get_nonexistent_item(temp_db_path): # Try to get a nonexistent item command = GetCommand( id="nonexistent-id", db_path=temp_db_path ) result = get(command) # Should return None assert result is None def test_remove_item(item_id): id, db_path = item_id # Remove the item command = RemoveCommand( id=id, db_path=db_path ) result = remove(command) # Should return True indicating success assert result is True # Verify item is no longer in the database db = sqlite3.connect(db_path) cursor = db.execute("SELECT COUNT(*) FROM POCKET_PICK WHERE id = ?", (id,)) count = cursor.fetchone()[0] db.close() assert count == 0 # Trying to get the removed item should return None get_command = GetCommand( id=id, db_path=db_path ) get_result = get(get_command) assert get_result is None def test_remove_nonexistent_item(temp_db_path): # Try to remove a nonexistent item command = RemoveCommand( id="nonexistent-id", db_path=temp_db_path ) result = remove(command) # Should return False indicating failure assert result is False </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_to_file_by_id.py"> import pytest import tempfile import os from pathlib import Path import json import sqlite3 from ...modules.data_types import AddCommand, ToFileByIdCommand, PocketItem from ...modules.functionality.add import add from ...modules.functionality.to_file_by_id import to_file_by_id @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def sample_item(temp_db_path): # Add a sample item to the database and return it command = AddCommand( text="This is sample content for testing to_file_by_id function", tags=["test", "sample"], db_path=temp_db_path ) return add(command) def test_to_file_by_id_successful(temp_db_path, sample_item): # Create a temporary output file path fd, output_path = tempfile.mkstemp() os.close(fd) os.unlink(output_path) # Remove the file so we can test creation try: # Create command to write content to file command = ToFileByIdCommand( id=sample_item.id, output_file_path_abs=output_path, db_path=temp_db_path ) # Write content to file result = to_file_by_id(command) # Verify result is True assert result is True # Verify file was created with correct content assert os.path.exists(output_path) with open(output_path, 'r', encoding='utf-8') as f: content = f.read() assert content == sample_item.text finally: # Clean up the temp file if os.path.exists(output_path): os.unlink(output_path) def test_to_file_by_id_nonexistent_id(temp_db_path): # Create a temporary output file path fd, output_path = tempfile.mkstemp() os.close(fd) os.unlink(output_path) # Remove the file so we can test creation try: # Create command with non-existent ID command = ToFileByIdCommand( id="nonexistent-id", output_file_path_abs=output_path, db_path=temp_db_path ) # Attempt to write content to file result = to_file_by_id(command) # Verify result is False assert result is False # Verify file was not created assert not os.path.exists(output_path) finally: # Clean up the temp file if it was created if os.path.exists(output_path): os.unlink(output_path) def test_to_file_by_id_creates_directories(temp_db_path, sample_item): # Create a temporary directory temp_dir = tempfile.mkdtemp() try: # Create a path with nested directories that don't exist output_path = os.path.join(temp_dir, "nested", "dirs", "output.txt") # Create command to write content to file command = ToFileByIdCommand( id=sample_item.id, output_file_path_abs=output_path, db_path=temp_db_path ) # Write content to file result = to_file_by_id(command) # Verify result is True assert result is True # Verify file was created with correct content assert os.path.exists(output_path) with open(output_path, 'r', encoding='utf-8') as f: content = f.read() assert content == sample_item.text finally: # Clean up the temp dir import shutil shutil.rmtree(temp_dir) def test_to_file_by_id_handles_errors(temp_db_path, sample_item, monkeypatch): # Mock the open function to raise a PermissionError def mock_open_with_permission_error(*args, **kwargs): raise PermissionError("Permission denied") # Create a temporary output file path fd, output_path = tempfile.mkstemp() os.close(fd) os.unlink(output_path) try: # Create command to write content to file command = ToFileByIdCommand( id=sample_item.id, output_file_path_abs=output_path, db_path=temp_db_path ) # Monkeypatch the built-in open function monkeypatch.setattr("builtins.open", mock_open_with_permission_error) # Attempt to write content to file result = to_file_by_id(command) # Verify result is False because of permission error assert result is False finally: # Clean up the temp file if it was created if os.path.exists(output_path): os.unlink(output_path) </file> <file path="src/mcp_server_pocket_pick/tests/__init__.py"> # Tests package initialization </file> <file path="src/mcp_server_pocket_pick/tests/test_init_db.py"> import pytest import tempfile import os from pathlib import Path import sqlite3 from ..modules.init_db import init_db, normalize_tag, normalize_tags def test_init_db(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) try: # Initialize database with the temp path db_path = Path(path) db = init_db(db_path) # Verify connection is open assert isinstance(db, sqlite3.Connection) # Verify POCKET_PICK table exists cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='POCKET_PICK'") assert cursor.fetchone() is not None # Verify indexes exist cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_created'") assert cursor.fetchone() is not None cursor = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_pocket_pick_text'") assert cursor.fetchone() is not None # Close the connection db.close() finally: # Clean up the temp file if os.path.exists(path): os.unlink(path) def test_normalize_tag(): # Test lowercase conversion assert normalize_tag("TAG") == "tag" # Test whitespace trimming assert normalize_tag(" tag ") == "tag" # Test space replacement assert normalize_tag("my tag") == "my-tag" # Test underscore replacement assert normalize_tag("my_tag") == "my-tag" # Test combined operations assert normalize_tag(" MY_TAG with SPACES ") == "my-tag-with-spaces" def test_normalize_tags(): tags = ["TAG1", " tag2 ", "my_tag3", "My Tag4"] normalized = normalize_tags(tags) assert normalized == ["tag1", "tag2", "my-tag3", "my-tag4"] </file> <file path="src/mcp_server_pocket_pick/__main__.py"> from mcp_server_pocket_pick import main main() </file> <file path=".python-version"> 3.10 </file> <file path="uv.lock"> version = 1 requires-python = ">=3.10" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anyio" version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } wheels = [ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "httpcore" version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "mcp" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "uvicorn" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 } wheels = [ { url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 }, ] [[package]] name = "mcp-server-pocket-pick" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "click" }, { name = "mcp" }, { name = "pydantic" }, ] [package.dev-dependencies] dev = [ { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "pydantic", specifier = ">=2.0.0" }, ] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8.0.0" }] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pydantic" version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, ] [[package]] name = "pydantic-core" version = "2.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } wheels = [ { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, ] [[package]] name = "pydantic-settings" version = "2.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "python-dotenv" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sse-starlette" version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, ] [[package]] name = "starlette" version = "0.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "uvicorn" version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] </file> <file path="specs/pocket-pick-v1.md"> # Pocket Pick - Your Personal Knowledge Base 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? 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. To implement this we'll... 1. Build the key sqlite functionality 2. Test the functionality with pytest 3. Expose the functionality via MCP server. ## SQLITE Database Structure ``` CREATE TABLE if not exists POCKET_PICK { id: str, created: datetime, text: str, tags: str[], } ``` ## Implementation Notes - DEFAULT_SQLITE_DATABASE_PATH = Path.home() / ".pocket_pick.db" - place in constants.py - always force (auto update) tags to be lowercase, trim whitespace, and use dash instead of spaces or underscores. - mcp comands will return whatever the command returns. - mirror ai_docs/mcp-server-git-repomix-output.xml structure to understand how to setup the mcp server - use ai_docs/paic-pkb-repomix-output.xml to get a rough understanding of what we're building. - libraries should be - click - mcp - pydantic - pytest (dev dependency) - sqlite3 (standard library) - use `uv add <package>` to add libraries. - we're using uv to manage the project. - add mcp-server-pocket-pick = "mcp_server_pocket_pick:main" to the project.scripts section in pyproject.toml ## API ``` pocket add <text> \ --tags, t: str[] (optional) --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket find <text> \ --mode: substr | fts | glob | regex | exact (optional) \ --limit, -l: number = 5 \ --info, -i: bool (show with metadata like id) \ --tags, -t: str[] (optional) \ --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket list \ --tags, -t: str[] (optional) \ --limit, -l: number = 100 \ --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket list-tags \ --limit, -l: number = 1000 \ --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket remove \ --id, -i: str \ --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket get \ --id, -i: str \ --db: str = DEFAULT_SQLITE_DATABASE_PATH pocket backup <backup_absolute_path> \ --db: str = DEFAULT_SQLITE_DATABASE_PATH ``` ### Example API Calls (for find modes) ``` # basic sqlite substring search pocket find "test" --mode substr # full text search pocket find "test" --mode fts # glob search pocket find "test*" --mode glob # regex search pocket find "^start.*test.*$" --mode regex # exact search pocket find "match exactly test" --mode exact ``` ## Project Structure - src/ - mcp_server_pocket_pick/ - __init__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml - __main__.py - MIRROR ai_docs/mcp-server-git-repomix-output.xml - server.py - MIRROR but use our functionality - serve(sqlite_database: Path | None) -> None - pass sqlite_database to every tool call (--db arg) - modules/ - __init__.py - init_db.py - data_types.py - class AddCommand(BaseModel) {text: str, tags: list[str] = [], db_path: Path = DEFAULT_SQLITE_DATABASE_PATH} - ... - constants.py - DEFAULT_SQLITE_DATABASE_PATH: Path = Path.home() / ".pocket_pick.db" - functionality/ - add.py - find.py - list.py - list_tags.py - remove.py - get.py - backup.py - tests/ - __init__.py - test_init_db.py - functionality/ - test_add.py - test_find.py - test_list.py - test_list_tags.py - test_remove.py - test_get.py - test_backup.py ## Validation (close the loop) - use `uv run pytest` to validate the tests pass. - use `uv run mcp-server-pocket-pick --help` to validate the mcp server works. </file> <file path="src/mcp_server_pocket_pick/modules/functionality/find.py"> import sqlite3 import json from datetime import datetime from typing import List import logging import re from ..data_types import FindCommand, PocketItem from ..init_db import init_db, normalize_tags logger = logging.getLogger(__name__) def find(command: FindCommand) -> List[PocketItem]: """ Find items in the pocket pick database matching the search criteria Args: command: FindCommand with search parameters Returns: List[PocketItem]: List of matching items """ # Normalize tags normalized_tags = normalize_tags(command.tags) if command.tags else [] # Connect to database db = init_db(command.db_path) try: # Base query query = "SELECT id, created, text, tags FROM POCKET_PICK" params = [] where_clauses = [] # Apply search mode if command.text: if command.mode == "substr": where_clauses.append("text LIKE ?") params.append(f"%{command.text}%") elif command.mode == "fts": try: # First, try using FTS5 virtual table # Replace normal query with FTS query query = """ SELECT POCKET_PICK.id, POCKET_PICK.created, POCKET_PICK.text, POCKET_PICK.tags FROM pocket_pick_fts JOIN POCKET_PICK ON pocket_pick_fts.rowid = POCKET_PICK.rowid """ # FTS5 query syntax if command.mode == "fts": # Check for different query formats # Direct quoted phrase - user already provided quotes for exact phrases if command.text.startswith('"') and command.text.endswith('"'): # User wants exact phrase matching (e.g., "word1 word2") # Just use it directly - FTS5 understands quoted phrases search_term = command.text logger.debug(f"Using quoted phrase search: {search_term}") # Multi-word regular search elif ' ' in command.text: # Default: Match all terms independently (AND behavior) search_term = command.text # Single word search else: search_term = command.text else: search_term = command.text # Using standard FTS5 query approach # Set up FTS5 query parameters where_clauses = [f"pocket_pick_fts MATCH ?"] params = [search_term] # FTS5 table doesn't have these columns, so we need to add tags filter separately if normalized_tags: tag_clauses = [] for tag in normalized_tags: tag_clauses.append("POCKET_PICK.tags LIKE ?") params.append(f"%\"{tag}\"%") where_clauses.append(f"({' AND '.join(tag_clauses)})") # We'll handle the query execution in a special way use_fts5 = True except sqlite3.OperationalError: # Fallback to basic LIKE-based search if FTS5 is not available logger.warning("FTS5 not available, falling back to basic search") use_fts5 = False # Standard fallback approach (original implementation) search_words = command.text.split() word_clauses = [] for word in search_words: word_clauses.append("text LIKE ?") params.append(f"%{word}%") where_clauses.append(f"({' AND '.join(word_clauses)})") elif command.mode == "glob": where_clauses.append("text GLOB ?") params.append(command.text) elif command.mode == "regex": # We'll need to filter with regex after query pass elif command.mode == "exact": where_clauses.append("text = ?") params.append(command.text) # Apply tag filter if tags are specified if normalized_tags: # Find items that have all the specified tags # We need to check if each tag exists in the JSON array tag_clauses = [] for tag in normalized_tags: tag_clauses.append("tags LIKE ?") # Use JSON substring matching, looking for the tag surrounded by quotes and commas or brackets params.append(f"%\"{tag}\"%") where_clauses.append(f"({' AND '.join(tag_clauses)})") # Handle query construction based on whether we're using FTS5 if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: # For FTS5, we've already constructed the base query if where_clauses: query += f" WHERE {' AND '.join(where_clauses)}" # Special ordering for FTS5 to get the best matches first query += f" ORDER BY rank, created DESC LIMIT {command.limit}" logger.debug(f"Using FTS5 query: {query}") else: # Standard query construction if where_clauses: query += f" WHERE {' AND '.join(where_clauses)}" # Apply limit query += f" ORDER BY created DESC LIMIT {command.limit}" # Execute query try: cursor = db.execute(query, params) except sqlite3.OperationalError as e: # If the FTS5 query fails, fall back to the basic query if command.mode == "fts" and 'use_fts5' in locals() and use_fts5: logger.warning(f"FTS5 query failed: {e}. Falling back to basic search.") # Reset to base query query = "SELECT id, created, text, tags FROM POCKET_PICK" params = [] # Standard fallback approach if command.text: search_words = command.text.split() word_clauses = [] for word in search_words: word_clauses.append("text LIKE ?") params.append(f"%{word}%") query += f" WHERE ({' AND '.join(word_clauses)})" # Re-add tag filters if needed if normalized_tags: tag_clauses = [] for tag in normalized_tags: tag_clauses.append("tags LIKE ?") params.append(f"%\"{tag}\"%") query += f" AND ({' AND '.join(tag_clauses)})" query += f" ORDER BY created DESC LIMIT {command.limit}" cursor = db.execute(query, params) else: # If it's not an FTS5 issue, re-raise the exception raise # Process results results = [] for row in cursor.fetchall(): id, created_str, text, tags_json = row # Parse the created timestamp created = datetime.fromisoformat(created_str) # Parse the tags JSON tags = json.loads(tags_json) # Create item item = PocketItem( id=id, created=created, text=text, tags=tags ) # Apply regex filter if needed (we do this after the SQL query) if command.mode == "regex" and command.text: try: pattern = re.compile(command.text, re.IGNORECASE) if not pattern.search(text): continue except re.error: logger.warning(f"Invalid regex pattern: {command.text}") continue results.append(item) return results except Exception as e: logger.error(f"Error finding items: {e}") raise finally: db.close() </file> <file path="src/mcp_server_pocket_pick/modules/data_types.py"> from pathlib import Path from pydantic import BaseModel from typing import List, Optional from datetime import datetime from .constants import DEFAULT_SQLITE_DATABASE_PATH class AddCommand(BaseModel): text: str tags: List[str] = [] db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class AddFileCommand(BaseModel): file_path: str tags: List[str] = [] db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class FindCommand(BaseModel): text: str mode: str = "substr" # substr | fts | glob | regex | exact limit: int = 5 info: bool = False tags: List[str] = [] db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class ListCommand(BaseModel): tags: List[str] = [] limit: int = 100 db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class ListTagsCommand(BaseModel): limit: int = 1000 db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class RemoveCommand(BaseModel): id: str db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class GetCommand(BaseModel): id: str db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class BackupCommand(BaseModel): backup_path: Path db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class ToFileByIdCommand(BaseModel): id: str output_file_path_abs: Path db_path: Path = DEFAULT_SQLITE_DATABASE_PATH class PocketItem(BaseModel): id: str created: datetime text: str tags: List[str] </file> <file path="src/mcp_server_pocket_pick/tests/functionality/test_find.py"> import pytest import tempfile import os from pathlib import Path import json import sqlite3 from datetime import datetime from ...modules.data_types import AddCommand, FindCommand, PocketItem from ...modules.functionality.add import add from ...modules.functionality.find import find from ...modules.init_db import init_db @pytest.fixture def temp_db_path(): # Create a temporary file path fd, path = tempfile.mkstemp() os.close(fd) # Return the path as a Path object yield Path(path) # Clean up the temp file after test if os.path.exists(path): os.unlink(path) @pytest.fixture def populated_db(temp_db_path): # Create sample items items = [ {"text": "Python programming is fun", "tags": ["python", "programming", "fun"]}, {"text": "SQL databases are powerful", "tags": ["sql", "database", "programming"]}, {"text": "Testing code is important", "tags": ["testing", "code", "programming"]}, {"text": "Regular expressions can be complex", "tags": ["regex", "programming", "advanced"]}, {"text": "Learning new technologies is exciting", "tags": ["learning", "technology", "fun"]} ] # Add items to the database for item in items: command = AddCommand( text=item["text"], tags=item["tags"], db_path=temp_db_path ) add(command) return temp_db_path def test_find_substr(populated_db): # Search for "programming" substring command = FindCommand( text="programming", mode="substr", limit=10, db_path=populated_db ) results = find(command) # Should match "Python programming is fun" assert len(results) == 1 assert "Python programming is fun" in [r.text for r in results] def test_find_fts(populated_db): # Test basic FTS search with a single word command = FindCommand( text="SQL", mode="fts", limit=10, db_path=populated_db ) results = find(command) # Should match "SQL databases are powerful" assert len(results) == 1 assert "SQL databases are powerful" in [r.text for r in results] def test_find_fts_phrase(populated_db): # Test FTS with a phrase (multiple words in exact order) command = FindCommand( text="Regular expressions", mode="fts", limit=10, db_path=populated_db ) results = find(command) # Should match "Regular expressions can be complex" assert len(results) == 1 assert "Regular expressions can be complex" in [r.text for r in results] def test_find_fts_multi_term(populated_db): # Test FTS with multiple terms (not necessarily in order) command = FindCommand( text="programming fun", mode="fts", limit=10, db_path=populated_db ) results = find(command) # Should match items containing both "programming" and "fun" assert len(results) > 0 # Check that all results contain both "programming" AND "fun" for result in results: assert "programming" in result.text.lower() and "fun" in result.text.lower() def test_find_fts_with_tags(populated_db): # Test FTS with tag filtering command = FindCommand( text="programming", mode="fts", tags=["fun"], # Only items tagged with "fun" limit=10, db_path=populated_db ) results = find(command) # Should match items containing "programming" AND tagged with "fun" assert len(results) == 1 assert "Python programming is fun" in [r.text for r in results] def test_find_fts_exact_phrase(populated_db): """ Test exact phrase matching functionality. This test is simplified to focus on the core functionality without relying on specific matching patterns that might be hard to reproduce with FTS5. """ # First make sure we have a known item with a specific phrase command = AddCommand( text="This contains programming fun as a phrase", tags=["test", "phrase"], db_path=populated_db ) result1 = add(command) # Add an item with same words but in reverse order command = AddCommand( text="This has fun programming in reverse order", tags=["test", "reverse"], db_path=populated_db ) result2 = add(command) # Search using quoted exact phrase matching command = FindCommand( text='"programming fun"', # The quotes force exact phrase matching in FTS5 mode="fts", limit=10, db_path=populated_db ) results = find(command) # Verify that our item with the exact phrase is found # And the item with reversed words is not found found_exact = "This contains programming fun as a phrase" in [r.text for r in results] found_reverse = "This has fun programming in reverse order" in [r.text for r in results] assert found_exact, "Should find item with exact phrase" assert not found_reverse, "Should not find item with reverse word order" def test_find_glob(populated_db): # Search for text starting with "Test" command = FindCommand( text="Test*", mode="glob", limit=10, db_path=populated_db ) results = find(command) # Should match "Testing code is important" assert len(results) == 1 assert "Testing code is important" in [r.text for r in results] def test_find_regex(populated_db): # Search for text containing "regular" (case insensitive) command = FindCommand( text=".*regular.*", mode="regex", limit=10, db_path=populated_db ) results = find(command) # Should match "Regular expressions can be complex" assert len(results) == 1 assert "Regular expressions can be complex" in [r.text for r in results] def test_find_exact(populated_db): # Search for exact match command = FindCommand( text="Learning new technologies is exciting", mode="exact", limit=10, db_path=populated_db ) results = find(command) # Should match exactly one item assert len(results) == 1 assert results[0].text == "Learning new technologies is exciting" def test_find_with_tags(populated_db): # Search for items with specific tags command = FindCommand( text="", # No text search tags=["fun"], limit=10, db_path=populated_db ) results = find(command) # Should match items with the "fun" tag assert len(results) == 2 assert "Python programming is fun" in [r.text for r in results] assert "Learning new technologies is exciting" in [r.text for r in results] def test_find_with_text_and_tags(populated_db): # Search for items with specific text and tags command = FindCommand( text="programming", mode="substr", tags=["fun"], limit=10, db_path=populated_db ) results = find(command) # Should match items with "programming" text and "fun" tag assert len(results) == 1 assert "Python programming is fun" in [r.text for r in results] def test_find_limit(populated_db): # Search with limit command = FindCommand( text="", # Match all limit=2, db_path=populated_db ) results = find(command) # Should only return 2 items (due to limit) assert len(results) == 2 </file> <file path="src/mcp_server_pocket_pick/__init__.py"> import click from pathlib import Path import logging import sys from .server import serve @click.command() @click.option("--database", "-d", type=Path, help="SQLite database path (default: ~/.pocket_pick.db)") @click.option("-v", "--verbose", count=True) def main(database: Path | None, verbose: bool) -> None: """Pocket Pick - Your Personal Knowledge Base""" import asyncio logging_level = logging.WARN if verbose == 1: logging_level = logging.INFO elif verbose >= 2: logging_level = logging.DEBUG logging.basicConfig(level=logging_level, stream=sys.stderr) asyncio.run(serve(database)) if __name__ == "__main__": main() </file> <file path="src/mcp_server_pocket_pick/modules/init_db.py"> import sqlite3 from pathlib import Path import logging logger = logging.getLogger(__name__) def init_db(db_path: Path) -> sqlite3.Connection: """Initialize SQLite database with POCKET_PICK table""" # Ensure parent directory exists db_path.parent.mkdir(parents=True, exist_ok=True) logger.info(f"Initializing database at {db_path}") # Ensure the directory exists if not db_path.parent.exists(): logger.info(f"Creating directory {db_path.parent}") db_path.parent.mkdir(parents=True, exist_ok=True) db = sqlite3.connect(str(db_path)) # Enable foreign keys db.execute("PRAGMA foreign_keys = ON") # Create the POCKET_PICK table db.execute(""" CREATE TABLE IF NOT EXISTS POCKET_PICK ( id TEXT PRIMARY KEY, created TIMESTAMP NOT NULL, text TEXT NOT NULL, tags TEXT NOT NULL ) """) # Create indexes for efficient searching db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_created ON POCKET_PICK(created)") db.execute("CREATE INDEX IF NOT EXISTS idx_pocket_pick_text ON POCKET_PICK(text)") # Create FTS5 virtual table for full-text search try: db.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS pocket_pick_fts USING fts5( text, content='POCKET_PICK', content_rowid='rowid' ) """) # Create triggers to keep FTS index up to date db.execute(""" CREATE TRIGGER IF NOT EXISTS pocket_pick_ai AFTER INSERT ON POCKET_PICK BEGIN INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); END """) db.execute(""" CREATE TRIGGER IF NOT EXISTS pocket_pick_ad AFTER DELETE ON POCKET_PICK BEGIN INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); END """) db.execute(""" CREATE TRIGGER IF NOT EXISTS pocket_pick_au AFTER UPDATE ON POCKET_PICK BEGIN INSERT INTO pocket_pick_fts(pocket_pick_fts, rowid, text) VALUES('delete', old.rowid, old.text); INSERT INTO pocket_pick_fts(rowid, text) VALUES (new.rowid, new.text); END """) # Rebuild FTS index if needed (for existing data) db.execute(""" INSERT OR IGNORE INTO pocket_pick_fts(rowid, text) SELECT rowid, text FROM POCKET_PICK """) except sqlite3.OperationalError as e: # If FTS5 is not available, log a warning but continue logger.warning(f"FTS5 extension not available: {e}. Full-text search will fallback to basic search.") # Commit changes db.commit() return db def normalize_tag(tag: str) -> str: """ Normalize tags: - lowercase - trim whitespace - replace spaces and underscores with dashes """ tag = tag.lower().strip() return tag.replace(' ', '-').replace('_', '-') def normalize_tags(tags: list[str]) -> list[str]: """Apply normalization to a list of tags""" return [normalize_tag(tag) for tag in tags] </file> <file path="src/mcp_server_pocket_pick/server.py"> import logging from pathlib import Path from typing import Sequence, List from mcp.server import Server from mcp.server.session import ServerSession from mcp.server.stdio import stdio_server from mcp.types import ( ClientCapabilities, TextContent, Tool, ListRootsResult, RootsCapability, ) from enum import Enum from pydantic import BaseModel from .modules.data_types import ( AddCommand, AddFileCommand, FindCommand, ListCommand, ListTagsCommand, RemoveCommand, GetCommand, BackupCommand, ToFileByIdCommand, ) from .modules.functionality.add import add from .modules.functionality.add_file import add_file from .modules.functionality.find import find from .modules.functionality.list import list_items from .modules.functionality.list_tags import list_tags from .modules.functionality.remove import remove from .modules.functionality.get import get from .modules.functionality.backup import backup from .modules.functionality.to_file_by_id import to_file_by_id from .modules.constants import DEFAULT_SQLITE_DATABASE_PATH logger = logging.getLogger(__name__) class PocketAdd(BaseModel): text: str tags: List[str] = [] db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketAddFile(BaseModel): file_path: str tags: List[str] = [] db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketFind(BaseModel): text: str mode: str = "substr" limit: int = 5 info: bool = False tags: List[str] = [] db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketList(BaseModel): tags: List[str] = [] limit: int = 100 db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketListTags(BaseModel): limit: int = 1000 db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketRemove(BaseModel): id: str db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketGet(BaseModel): id: str db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketBackup(BaseModel): backup_path: str db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketToFileById(BaseModel): id: str output_file_path_abs: str db: str = str(DEFAULT_SQLITE_DATABASE_PATH) class PocketTools(str, Enum): ADD = "pocket_add" ADD_FILE = "pocket_add_file" FIND = "pocket_find" LIST = "pocket_list" LIST_TAGS = "pocket_list_tags" REMOVE = "pocket_remove" GET = "pocket_get" BACKUP = "pocket_backup" TO_FILE_BY_ID = "pocket_to_file_by_id" async def serve(sqlite_database: Path | None = None) -> None: logger.info(f"Starting Pocket Pick MCP server") # Determine which database path to use db_path = sqlite_database if sqlite_database is not None else DEFAULT_SQLITE_DATABASE_PATH logger.info(f"Using database at {db_path}") # Initialize the database at startup to ensure it exists from .modules.init_db import init_db connection = init_db(db_path) connection.close() logger.info(f"Database initialized at {db_path}") server = Server("pocket-pick") @server.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name=PocketTools.ADD, description="Add a new item to your pocket pick database", inputSchema=PocketAdd.schema(), ), Tool( name=PocketTools.ADD_FILE, description="Add a new item to your pocket pick database from a file", inputSchema=PocketAddFile.schema(), ), Tool( name=PocketTools.FIND, description="Find items in your pocket pick database by text and tags", inputSchema=PocketFind.schema(), ), Tool( name=PocketTools.LIST, description="List items in your pocket pick database, optionally filtered by tags", inputSchema=PocketList.schema(), ), Tool( name=PocketTools.LIST_TAGS, description="List all tags in your pocket pick database with their counts", inputSchema=PocketListTags.schema(), ), Tool( name=PocketTools.REMOVE, description="Remove an item from your pocket pick database by ID", inputSchema=PocketRemove.schema(), ), Tool( name=PocketTools.GET, description="Get an item from your pocket pick database by ID", inputSchema=PocketGet.schema(), ), Tool( name=PocketTools.BACKUP, description="Backup your pocket pick database to a specified location", inputSchema=PocketBackup.schema(), ), Tool( name=PocketTools.TO_FILE_BY_ID, description="Write a pocket pick item's content to a file by its ID (requires absolute file path)", inputSchema=PocketToFileById.schema(), ), ] @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: # Override db_path if provided via command line if sqlite_database is not None: arguments["db"] = str(sqlite_database) elif "db" not in arguments: # Use default if not specified arguments["db"] = str(DEFAULT_SQLITE_DATABASE_PATH) db_path = Path(arguments["db"]) # Ensure the database exists and is initialized for every command from .modules.init_db import init_db connection = init_db(db_path) connection.close() match name: case PocketTools.ADD: command = AddCommand( text=arguments["text"], tags=arguments.get("tags", []), db_path=db_path ) result = add(command) return [TextContent( type="text", text=f"Added item with ID: {result.id}\nText: {result.text}\nTags: {', '.join(result.tags)}" )] case PocketTools.ADD_FILE: command = AddFileCommand( file_path=arguments["file_path"], tags=arguments.get("tags", []), db_path=db_path ) result = add_file(command) return [TextContent( type="text", text=f"Added file content with ID: {result.id}\nFrom file: {arguments['file_path']}\nTags: {', '.join(result.tags)}" )] case PocketTools.FIND: command = FindCommand( text=arguments["text"], mode=arguments.get("mode", "substr"), limit=arguments.get("limit", 5), info=arguments.get("info", False), tags=arguments.get("tags", []), db_path=db_path ) results = find(command) if not results: return [TextContent( type="text", text="No items found matching your search criteria." )] output = [] for item in results: if command.info: output.append(f"ID: {item.id}") output.append(f"Created: {item.created.isoformat()}") output.append(f"Tags: {', '.join(item.tags)}") output.append(f"Text: {item.text}") output.append("") else: output.append(item.text) output.append("") return [TextContent( type="text", text="\n".join(output).strip() )] case PocketTools.LIST: command = ListCommand( tags=arguments.get("tags", []), limit=arguments.get("limit", 100), db_path=db_path ) results = list_items(command) if not results: return [TextContent( type="text", text="No items found." )] output = [] for item in results: output.append(f"ID: {item.id}") output.append(f"Created: {item.created.isoformat()}") output.append(f"Tags: {', '.join(item.tags)}") output.append(f"Text: {item.text}") output.append("") return [TextContent( type="text", text="\n".join(output).strip() )] case PocketTools.LIST_TAGS: command = ListTagsCommand( limit=arguments.get("limit", 1000), db_path=db_path ) results = list_tags(command) if not results: return [TextContent( type="text", text="No tags found." )] output = ["Tags:"] for item in results: output.append(f"{item['tag']} ({item['count']})") return [TextContent( type="text", text="\n".join(output) )] case PocketTools.REMOVE: command = RemoveCommand( id=arguments["id"], db_path=db_path ) result = remove(command) if result: return [TextContent( type="text", text=f"Item {command.id} removed successfully." )] else: return [TextContent( type="text", text=f"Item {command.id} not found." )] case PocketTools.GET: command = GetCommand( id=arguments["id"], db_path=db_path ) result = get(command) if result: return [TextContent( type="text", text=f"ID: {result.id}\nCreated: {result.created.isoformat()}\nTags: {', '.join(result.tags)}\nText: {result.text}" )] else: return [TextContent( type="text", text=f"Item {command.id} not found." )] case PocketTools.BACKUP: command = BackupCommand( backup_path=Path(arguments["backup_path"]), db_path=db_path ) result = backup(command) if result: return [TextContent( type="text", text=f"Database backed up successfully to {command.backup_path}" )] else: return [TextContent( type="text", text=f"Failed to backup database to {command.backup_path}" )] case PocketTools.TO_FILE_BY_ID: command = ToFileByIdCommand( id=arguments["id"], output_file_path_abs=Path(arguments["output_file_path_abs"]), db_path=db_path ) result = to_file_by_id(command) if result: return [TextContent( type="text", text=f"Content written successfully to {command.output_file_path_abs}" )] else: return [TextContent( type="text", text=f"Failed to write content to {command.output_file_path_abs}" )] case _: raise ValueError(f"Unknown tool: {name}") options = server.create_initialization_options() async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, options, raise_exceptions=True) </file> <file path=".gitignore"> # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # mypy .mypy_cache/ .dmypy.json dmypy.json # Editors .vscode/ .idea/ *.swp *.swo ai_docs/paic-pkb-repomix-output.xml </file> <file path="pyproject.toml"> [project] name = "mcp-server-pocket-pick" version = "0.1.0" description = "Your Personal Knowledge Base with MCP" readme = "README.md" authors = [ { name = "IndyDevDan", email = "[email protected]" } ] requires-python = ">=3.10" dependencies = [ "click>=8.1.7", "mcp>=1.0.0", "pydantic>=2.0.0", ] [project.scripts] mcp-server-pocket-pick = "mcp_server_pocket_pick:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv] dev-dependencies = ["pytest>=8.0.0"] </file> <file path="README.md"> # Pocket Pick (MCP Server) > See how we used AI Coding, Claude Code, and MCP to build this tool on the [@IndyDevDan youtube channel](https://youtu.be/d-SyGA0Avtw). 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? 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. <img src="./images/pocket-pick.png" alt="Pocket Pick" style="max-width: 600px;"> ## Features - **Personal Knowledge Base**: Store code snippets, information, and ideas - **Tag-Based Organization**: Add tags to categorize and filter your knowledge - **Flexible Search**: Find content using substring, full-text, glob, regex, or exact matching - **MCP Integration**: Seamlessly works with Claude and other MCP-compatible AI assistants - **SQLite Backend**: Fast, reliable, and portable database storage - **Command-Line Interface**: Easy to use from the terminal ## Installation Install [uv](https://docs.astral.sh/uv/getting-started/installation/) ```bash # Clone the repository git clone https://github.com/indydevdan/pocket-pick.git cd pocket-pick # Install dependencies uv sync ``` Usage from JSON format Default Database for Claude Code ```json { "command": "uv", "args": ["--directory", ".", "run", "mcp-server-pocket-pick"] } ``` Custom Database for Claude Code ```json { "command": "uv", "args": ["--directory", ".", "run", "mcp-server-pocket-pick", "--database", "./database.db"] } ``` ## Usage with Claude Code ```bash # Add the pocket-pick server to Claude Code (if you're in the directory) claude mcp add pocket-pick -- \ uv --directory . \ run mcp-server-pocket-pick # Add the pocket-pick server to Claude Code claude mcp add pocket-pick -- \ uv --directory /path/to/pocket-pick-codebase \ run mcp-server-pocket-pick # With custom database location claude mcp add pocket-pick -- \ uv --directory /path/to/pocket-pick-codebase \ run mcp-server-pocket-pick --database ./database.db # List existing MCP servers - Validate that the server is running claude mcp list # Start claude code claude ``` ## Pocket Pick MCP Tools The following MCP tools are available in Pocket Pick: | Tool | Description | | -------------------- | -------------------------------------------- | | `pocket_add` | Add a new item to your knowledge base | | `pocket_add_file` | Add a file's content to your knowledge base | | `pocket_find` | Find items by text and/or tags | | `pocket_list` | List all items, optionally filtered by tags | | `pocket_list_tags` | List all tags with their counts | | `pocket_remove` | Remove an item by ID | | `pocket_get` | Get a specific item by ID | | `pocket_backup` | Backup the database | | `pocket_to_file_by_id` | Write an item's content to a file by its ID (requires absolute path) | ## Using with Claude After setting up Pocket Pick as an MCP server for Claude Code, you can use it your conversations: ### Adding Items Add items directly ```bash Add "claude mcp list" as a pocket pick item. tags: mcp, claude, code ``` Add items from clipboard ```bash pbpaste and create a pocket pick item with the following tags: python, algorithm, fibonacci ``` Add items from a file ```bash Add the contents of ~/Documents/code-snippets/fibonacci.py to pocket pick with tags: python, algorithm, fibonacci ``` ### Listing Items List all items or tags: ``` list all my pocket picks ``` ### Finding Items Search for items in your knowledge base with tags ``` List pocket pick items with python and mcp tags ``` Search for text with specific content ``` pocket pick find "python" ``` ### Get or Remove Items Get or remove specific items: ``` get the pocket pick item with ID 1234-5678-90ab-cdef remove the pocket pick item with ID 1234-5678-90ab-cdef ``` ### Export to File 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: ``` export the pocket pick item with ID 1234-5678-90ab-cdef to /Users/username/Documents/exported-snippet.py ``` The tool requires an absolute file path and will automatically create any necessary parent directories if they don't exist. ### Backup ``` backup the pocket pick database to ~/Documents/pocket-pick-backup.db ``` ## Search Modes Pocket Pick supports various search modes: - **substr**: (Default) Simple substring matching - **fts**: Full-text search with powerful capabilities: - Regular word search: Matches all words in any order (e.g., "python programming" finds entries with both words) - Exact phrase search: Use quotes for exact phrase matching (e.g., `"python programming"` only finds entries with that exact phrase) - **glob**: SQLite glob pattern matching (e.g., "test*" matches entries starting with "test") - **regex**: Regular expression matching - **exact**: Exact string matching Example find commands: ``` Find items containing "pyt" using substring matching Find items containing "def fibonacci" using full text search Find items containing "test*" using glob pattern matching Find items containing "^start.*test.*$" using regular expression matching Find items containing "match exactly test" using exact string matching ``` ## Database Structure Pocket Pick uses a simple SQLite database with the following schema: ```sql CREATE TABLE POCKET_PICK ( id TEXT PRIMARY KEY, -- UUID identifier created TIMESTAMP NOT NULL, -- Creation timestamp text TEXT NOT NULL, -- Item content tags TEXT NOT NULL -- JSON array of tags ) ``` The database file is located at `~/.pocket_pick.db` by default. ## Development ### Running Tests ```bash # Run all tests uv run pytest # Run with verbose output uv run pytest -v ``` ### Running the Server Directly ```bash # Start the MCP server uv run mcp-server-pocket-pick # With verbose logging uv run mcp-server-pocket-pick -v # With custom database location uv run mcp-server-pocket-pick --database ./database.db ``` ## Other Useful MCP Servers ### Fetch ```bash claude mcp add http-fetch -- uvx mcp-server-fetch ``` --- 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) </file> </files> ```