Repository: qte77/context-engineering-template Files analyzed: 103 Estimated tokens: 233.4k Directory structure: └── qte77-context-engineering-template/ ├── README.md ├── AGENTS.md ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── Makefile ├── mkdocs.yaml ├── pyproject.toml ├── uv.lock ├── .env.example ├── .gitmessage ├── assets/ │ └── images/ │ ├── example_execute_feature_mcp_client_created.PNG │ ├── example_execute_feature_mcp_server_created.PNG │ ├── example_execute_feature_mcp_server_PRP_update_pyproject.PNG │ ├── example_execute_feature_mcp_server_PRP_update_pyproject_diff.PNG │ └── example_generate_feature_mcp_server_PRP.PNG ├── context/ │ └── templates/ │ ├── feature_base.md │ └── prp_base.md ├── docs/ │ ├── llms.txt │ └── architecture/ │ └── sequence_diagram.mermaid ├── examples/ │ └── mcp-server-client/ │ ├── README.md │ ├── Makefile │ ├── pyproject.toml │ ├── uv.lock │ ├── assets/ │ │ └── images/ │ ├── context/ │ │ ├── features/ │ │ │ ├── feature_1_mcp_server.md │ │ │ ├── feature_2_mcp_client.md │ │ │ └── feature_3_streamlit_gui.md │ │ ├── outputs/ │ │ │ ├── client_get_date_input.json │ │ │ ├── client_get_weather_input.json │ │ │ ├── client_invalid_tool.json │ │ │ ├── client_roll_dice_input.json │ │ │ ├── get_date_example.json │ │ │ ├── get_weather_example.json │ │ │ ├── roll_dice_example.json │ │ │ ├── streamlit_error_handling.json │ │ │ ├── streamlit_get_date_interaction.json │ │ │ ├── streamlit_get_weather_interaction.json │ │ │ └── streamlit_roll_dice_interaction.json │ │ ├── PRPs/ │ │ │ ├── feature_1_mcp_server.md │ │ │ ├── feature_2_mcp_client.md │ │ │ └── feature_3_streamlit_gui.md │ │ └── templates/ │ │ ├── feature_base.md │ │ └── prp_base.md │ ├── src/ │ │ ├── __init__.py │ │ ├── main.py │ │ ├── py.typed │ │ ├── gui/ │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ ├── components/ │ │ │ │ ├── __init__.py │ │ │ │ ├── connection.py │ │ │ │ ├── history.py │ │ │ │ └── tool_forms.py │ │ │ ├── models/ │ │ │ │ ├── __init__.py │ │ │ │ └── gui_models.py │ │ │ └── utils/ │ │ │ ├── __init__.py │ │ │ ├── async_helper.py │ │ │ ├── formatting.py │ │ │ ├── mcp_wrapper.py │ │ │ └── validation.py │ │ ├── mcp_client/ │ │ │ ├── __init__.py │ │ │ ├── cli.py │ │ │ ├── client.py │ │ │ ├── transport.py │ │ │ └── models/ │ │ │ ├── __init__.py │ │ │ └── responses.py │ │ └── mcp_server/ │ │ ├── __init__.py │ │ ├── server.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ └── requests.py │ │ └── tools/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── date_time.py │ │ ├── dice.py │ │ └── weather.py │ └── tests/ │ ├── __init__.py │ ├── test_cli.py │ ├── test_gui.py │ ├── test_mcp_client.py │ ├── test_mcp_server.py │ ├── fixtures/ │ │ ├── __init__.py │ │ └── mcp_messages.py │ └── test_tools/ │ ├── __init__.py │ ├── test_date_time.py │ ├── test_dice.py │ └── test_weather.py ├── src/ │ ├── __init__.py │ ├── main.py │ └── py.typed ├── .claude/ │ ├── settings.local.json │ └── commands/ │ ├── execute-prp.md │ └── generate-prp.md ├── .devcontainer/ │ └── setup_python_claude/ │ └── devcontainer.json └── .github/ ├── dependabot.yaml ├── scripts/ │ ├── create_pr.sh │ └── delete_branch_pr_tag.sh └── workflows/ ├── bump-my-version.yaml ├── codeql.yaml ├── generate-deploy-mkdocs-ghpages.yaml ├── links-fail-fast.yaml ├── pytest.yaml ├── ruff.yaml ├── summarize-jobs-reusable.yaml └── write-llms-txt.yaml ================================================ FILE: README.md ================================================ # Context Engineering Template This project aims to implement a template for context engineering with coding agents. As suggested by several resources, including [context-engineering-intro](https://github.com/coleam00/context-engineering-intro), [The rise of "context engineering"](https://blog.langchain.com/the-rise-of-context-engineering/), [Context Engineering](https://blog.langchain.com/context-engineering-for-agents/) and somewhat [He Built 40 Startups Using Just Prompts — Here’s His System](https://youtu.be/CIAu6WeckQ0). [![License](https://img.shields.io/badge/license-GNUGPLv3-green.svg)](LICENSE) ![Version](https://img.shields.io/badge/version-0.0.2-58f4c2) [![CodeQL](https://github.com/qte77/context-engineering-template/actions/workflows/codeql.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/codeql.yaml) [![CodeFactor](https://www.codefactor.io/repository/github/qte77/context-engineering-template/badge)](https://www.codefactor.io/repository/github/qte77/context-engineering-template) [![ruff](https://github.com/qte77/context-engineering-template/actions/workflows/ruff.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/ruff.yaml) [![pytest](https://github.com/qte77/context-engineering-template/actions/workflows/pytest.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/pytest.yaml) [![Link Checker](https://github.com/qte77/context-engineering-template/actions/workflows/links-fail-fast.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/links-fail-fast.yaml) [![Deploy Docs](https://github.com/qte77/context-engineering-template/actions/workflows/generate-deploy-mkdocs-ghpages.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/generate-deploy-mkdocs-ghpages.yaml) **DevEx** [![vscode.dev](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=vscode.dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://vscode.dev/github/qte77/context-engineering-template) [![Codespace Python Claude](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Codespace%20Dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://github.com/codespaces/new?repo=qte77/context-engineering-template&devcontainer_path=.devcontainer/setup_python_claude/devcontainer.json) [![TalkToGithub](https://img.shields.io/badge/TalkToGithub-7a83ff.svg)](https://talktogithub.com/qte77/context-engineering-template) [![llms.txt (UitHub)](https://img.shields.io/badge/llms.txt-uithub-800080.svg)](https://github.com/qte77/context-engineering-template) [![llms.txt (GitToDoc)](https://img.shields.io/badge/llms.txt-GitToDoc-fe4a60.svg)](https://gittodoc.com/qte77/context-engineering-template) ## Status (DRAFT) (WIP) ----> Not fully implemented yet For version history have a look at the [CHANGELOG](CHANGELOG.md). ## Purpose Let the Coding Agent do the heavy lifting. Build code base from top to bottom: Define Business Requirements (BRD) and afterwards features to be implemented. The goal could be to to implement some kind of guided top-down BDD: behavior > tests > implementation. ## Features - Runs tests, linting and type checks: only
Show Sequence Diagram Sequence Diagram Sequence Diagram
## Setup 1. `make setup_python_claude` 2. If .env to be used: `make export_env_file` ## Usage 1. Update [Agents.md](AGENTS.md) to your needs. 2. Describe desired feature in `/context/features/feature_XXX.md`, like shown in [feature_base.md](/context/templates/feature_base.md). 3. Place optional examples into [/context/examples](/context/examples). 4. Let the Product Requirements Prompt (PRP) be generated: - In Claude Code CLI: `/generate-prp feature_XXX.md` - or: `make prp_gen_claude "ARGS=feature_XXX.md"` 5. Let the feature be implemented based on the PRP: - In Claude Code CLI: `/execute-prp feature_XXX.md` - or: `make prp_exe_claude "ARGS=feature_XXX.md"` ### Configuration - General system behavior: `AGENTS.md`, redirected from `CLAUDE.md` - Claude settings: `.claude/settings.local.json` - CLaude commands: `.claude/commands` - Feature template: `context/templates/feature_base.md` - PRP template: `context/templates/prp_base.md` ### Environment [.env.example](.env.example) contains examples for usage of API keys and variables. ```text ANTHROPIC_API_KEY="sk-abc-xyz" GEMINI_API_KEY="xyz" GITHUB_API_KEY="ghp_xyz" ... ``` ## TODO - Implement business process as discussed in [He Built 40 Startups Using Just Prompts — Here’s His System](https://youtu.be/CIAu6WeckQ0) - Refine `AGENTS.md` to let the agent not do bulk but incremental changes, also implement tests first, then code and iterate until functional (red > green > blue). ================================================ FILE: AGENTS.md ================================================ # Agent instructions for `Agents-eval` repository As proposed by [agentsmd.net](https://agentsmd.net/) and used by [wandb weave AGENTS.md](https://github.com/wandb/weave/blob/master/AGENTS.md). ## Core Rules & AI Behavior * When you learn something new about the codebase or introduce a new concept, **update this file (`AGENTS.md`)** to reflect the new knowledge. This is YOUR FILE! It should grow and evolve with you. * If something doesn't make sense architecturally, from a developer experience standpoint, or product-wise, please add it to the **`Requests to Humans`** section below. * Always follow the established coding patterns, conventions, and architectural decisions documented here and in the `docs/` directory. * **Never assume missing context.** Ask questions if you are uncertain about requirements or implementation details. * **Never hallucinate libraries or functions.** Only use known, verified Python packages listed in `pyproject.toml`. * **Always confirm file paths and module names** exist before referencing them in code or tests. * **Never delete or overwrite existing code** unless explicitly instructed to or as part of a documented refactoring task. ## Architecture Overview This is a multi-agent evaluation system for assessing agentic AI systems. The project uses **PydanticAI** as the core framework for agent orchestration and is designed for evaluation purposes, not for production agent deployment. ### Data Flow 1. User input → Manager Agent 2. Manager delegates to Researcher Agent (with DuckDuckGo search) 3. Researcher results → Analyst Agent for validation 4. Validated data → Synthesizer Agent for report generation 5. Results evaluated using configurable metrics ### Key Dependencies * **PydanticAI**: Agent framework and orchestration * **uv**: Fast Python dependency management * **Streamlit**: GUI framework * **Ruff**: Code formatting and linting * **MyPy**: Static type checking ## Codebase Structure & Modularity ### Main Components * `src/app/`: The core application logic. This is where most of your work will be. * `main.py`: The main entry point for the CLI application. * `agents/agent_system.py`: Defines the multi-agent system, their interactions, and orchestration. **This is the central logic for agent behavior.** * `config/data_models.py`: Contains all **Pydantic** models that define the data contracts. This is a critical file for understanding data flow. * `config/config_chat.json`: Holds provider settings and system prompts for agents. * `config/config_eval.json`: Defines evaluation metrics and their weights. * `evals/metrics.py`: Implements the evaluation metrics. * `src/gui/`: Contains the source code for the Streamlit GUI. * `docs/`: Contains project documentation, including the Product Requirements Document (`PRD.md`) and the C4 architecture model. * `tests/`: Contains all tests for the project, written using **pytest**. ### Code Organization Rules * **Never create a file longer than 500 lines of code.** If a file approaches this limit, refactor by splitting it into smaller, more focused modules or helper files. * Organize code into clearly separated modules grouped by feature. * Use clear, consistent, and absolute imports within packages. ## Development Commands & Environment ### Environment Setup The project requirements are stated in `pyproject.toml`. Your development environment should be set up automatically using the provided `Makefile`, which configures the virtual environment. * `make setup_dev`: Install all dev dependencies. * `make setup_dev_claude`: Setup dev environment with Claude Code CLI. * `make setup_dev_ollama`: Setup dev environment with Ollama local LLM. ### Running the Application * `make run_cli`: Run the CLI application. * `make run_cli ARGS="--help"`: Run CLI with specific arguments. * `make run_gui`: Run the Streamlit GUI. ### Testing and Code Quality * `make test_all`: Run all tests with pytest. * `make coverage_all`: Run tests and generate a coverage report. * `make ruff`: Format code and fix linting issues with Ruff. * `make type_check`: Run mypy static type checking on `src/app/`. ## Testing & Reliability * **Always create Pytest unit tests** for new features (functions, classes, etc.). * Tests must live in the `tests/` folder, mirroring the `src/app` structure. * After updating any logic, check whether existing unit tests need to be updated. If so, do it. * For each new feature, include at least: * 1 test for the expected use case (happy path). * 1 test for a known edge case. * 1 test for an expected failure case (e.g., invalid input). * **To run a specific test file or function, use `uv run pytest` directly:** * `uv run pytest tests/test_specific_file.py` * `uv run pytest tests/test_specific_file.py::test_function` ## Style, Patterns & Documentation ### Coding Style * **Use Pydantic** models in `src/app/config/data_models.py` for all data validation and data contracts. **Always use or update these models** when modifying data flows. * Use the predefined error message functions from `src/app/utils/error_messages.py` for consistency. * When writing complex logic, **add an inline `# Reason:` comment** explaining the *why*, not just the *what*. * Comment non-obvious code to ensure it is understandable to a mid-level developer. ### Documentation * Write **docstrings for every function, class, and method** using the Google style format. This is critical as the documentation site is built automatically from docstrings. ```python def example_function(param1: int) -> str: """A brief summary of the function. Args: param1 (int): A description of the first parameter. Returns: str: A description of the return value. """ return "example" ``` * Update this `AGENTS.md` file when introducing new patterns or concepts. * Document significant architectural decisions in `docs/ADR.md`. * Document all significant changes, features, and bug fixes in `docs/CHANGELOG.md`. ## Code Review & PR Guidelines ### PR Requirements * **Title Format**: Commit messages and PR titles must follow the **Conventional Commits** specification, as outlined in the `.gitmessage` template. * Provide detailed PR summaries including the purpose of the changes and the testing performed. ### Pre-commit Checklist 1. Run the linter and formatter: `make ruff`. 2. Ensure all tests pass: `make test_all`. 3. Ensure static type checks pass: `make type_check`. 4. Update documentation as described below. ## Requests to Humans This section contains a list of questions, clarifications, or tasks that AI agents wish to have humans complete or elaborate on. * [ ] The `agent_system.py` module has a `NotImplementedError` for streaming with Pydantic model outputs. Please clarify the intended approach for streaming structured data. * [ ] The `llm_model_funs.py` module has `NotImplementedError` for the Gemini and HuggingFace providers. Please provide the correct implementation or remove them if they are not supported. * [ ] The `agent_system.py` module contains a `FIXME` note regarding the use of a try-catch context manager. Please review and implement the intended error handling. * [ ] Add TypeScript testing guidelines (if a TypeScript frontend is planned for the future). ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Guiding Principles - Changelogs are for humans, not machines. - There should be an entry for every single version. - The same types of changes should be grouped. - Versions and sections should be linkable. - The latest version comes first. - The release date of each version is displayed. - Mention whether you follow Semantic Versioning. ## Types of changes - `Added` for new features. - `Changed` for changes in existing functionality. - `Deprecated` for soon-to-be removed features. - `Removed` for now removed features. - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. ## [Unreleased] ### Added - Initial template containing templates for PRP ================================================ FILE: CLAUDE.md ================================================ @AGENTS.md ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Makefile ================================================ # This Makefile automates the build, test, and clean processes for the project. # It provides a convenient way to run common tasks using the 'make' command. # Run `make help` to see all available recipes. .SILENT: .ONESHELL: .PHONY: all setup_python_claude setup_dev setup_prod setup_claude_code prp_gen_claude prp_exe_claude ruff test_all check_types coverage_all output_unset_app_env_sh run_example_gui run_example_server run_example_client help .DEFAULT_GOAL := help ENV_FILE := .env SRC_PATH := src APP_PATH := $(SRC_PATH) EXAMPLES_PATH := examples/mcp-server-client FEAT_DEF_PATH := /context/features PRP_DEF_PATH := /context/PRPs PRP_CLAUDE_GEN_CMD := generate-prp PRP_CLAUDE_EXE_CMD := execute-prp # MARK: setup # construct the full paths and execute Claude Code commands # TODO switch folder by function called () # TODO Claude Code non-interactive headless mode tee to CLI define CLAUDE_PRP_RUNNER echo "Starting Claude Code PRP runner ..." dest_file=$(firstword $(strip $(1))) dest_cmd=$(firstword $(strip $(2))) if [ -z "$${dest_file}" ]; then echo "Error: ARGS for PRP filename is empty. Please provide a PRP filename." exit 1 fi case "$${dest_cmd}" in start) dest_cmd=$(PRP_CLAUDE_GEN_CMD) dest_path=$(FEAT_DEF_PATH);; stop) dest_cmd=$(PRP_CLAUDE_EXE_CMD) dest_path=$(PRP_DEF_PATH);; *) echo "Unknown command: $${dest_cmd}. Exiting ..." exit 1;; esac dest_cmd="/project:$${dest_cmd} $${dest_path}/$${dest_file}" echo "Executing command '$${dest_cmd}' ..." claude -p "$${dest_cmd}" 2>&1 claude -p "/cost" 2>&1 endef setup_python_claude: # Set up environment and install Claude Code CLI $(MAKE) -s setup_dev $(MAKE) -s export_env_file $(MAKE) -s setup_claude_code setup_dev: ## Install uv and deps, Download and start Ollama echo "Setting up dev environment ..." pip install uv -q uv sync --all-groups setup_prod: ## Install uv and deps, Download and start Ollama echo "Setting up prod environment ..." pip install uv -q uv sync --frozen setup_claude_code: ## Setup Claude Code CLI, node.js and npm have to be present echo "Setting up claude code ..." npm install -g @anthropic-ai/claude-code claude config set --global preferredNotifChannel terminal_bell echo "npm version: $$(npm --version)" claude --version export_env_file: # Read ENV_FILE and export k=v to env while IFS='=' read -r key value || [ -n "$${key}" ]; do case "$${key}" in ''|\#*) continue ;; esac value=$$(echo "$${value}" | sed -e 's/^"//' -e 's/"$$//') export "$${key}=$${value}" done < .env output_unset_env_sh: ## Unset app environment variables uf="./unset_env.sh" echo "Outputing '$${uf}' ..." printenv | awk -F= '/_API_KEY=/ {print "unset " $$1}' > $$uf # MARK: context engineering prp_gen_claude: ## generates the PRP from the file passed in "ARGS=file" $(call CLAUDE_PRP_RUNNER, $(ARGS), "generate") prp_exe_claude: ## executes the PRP from the file passed in "ARGS=file" $(call CLAUDE_PRP_RUNNER, $(ARGS), "execute") # MARK: code quality ruff: ## Lint: Format and check with ruff uv run ruff format uv run ruff check --fix test_all: ## Run all tests uv run pytest coverage_all: ## Get test coverage uv run coverage run -m pytest || true uv run coverage report -m check_types: ## Check for static typing errors uv run mypy $(APP_PATH) # MARK: run run_example_gui: ## Launch MCP server-client example GUI unset VIRTUAL_ENV && $(MAKE) -C $(EXAMPLES_PATH) run_gui run_example_server: ## Run MCP server-client example server unset VIRTUAL_ENV && $(MAKE) -C $(EXAMPLES_PATH) run_server run_example_client: ## Run MCP server-client example client unset VIRTUAL_ENV && $(MAKE) -C $(EXAMPLES_PATH) run_client ARGS="$(ARGS)" # MARK: help # TODO add stackoverflow source help: ## Displays this message with available recipes echo "Usage: make [recipe]" echo "Recipes:" awk '/^[a-zA-Z0-9_-]+:.*?##/ { helpMessage = match($$0, /## (.*)/) if (helpMessage) { recipe = $$1 sub(/:/, "", recipe) printf " \033[36m%-20s\033[0m %s\n", recipe, substr($$0, RSTART + 3, RLENGTH) } }' $(MAKEFILE_LIST) ================================================ FILE: mkdocs.yaml ================================================ --- # https://github.com/james-willett/mkdocs-material-youtube-tutorial # https://mkdocstrings.github.io/recipes/ # site info set in workflow site_name: '' site_description: '' repo_url: '' edit_uri: edit/main theme: name: material language: en features: - content.code.annotation - content.code.copy - content.tabs.link - navigation.footer - navigation.sections - navigation.tabs - navigation.top - toc.integrate - search.suggest - search.highlight palette: - media: "(prefers-color-scheme: light)" scheme: default toggle: # icon: material/brightness-7 icon: material/toggle-switch-off-outline name: "Toggle Dark Mode" - media: "(prefers-color-scheme: dark)" scheme: slate toggle: # icon: material/brightness-4 icon: material/toggle-switch name: "Toggle Light Mode" nav: - Home: index.md - Code: docstrings.md - Change Log: CHANGELOG.md - License: LICENSE - llms.txt: llms.txt plugins: - search: lang: en - autorefs - mkdocstrings: handlers: python: paths: [src] options: show_root_heading: true show_root_full_path: true show_object_full_path: false show_root_members_full_path: false show_category_heading: true show_submodules: true markdown_extensions: - attr_list - pymdownx.magiclink - pymdownx.tabbed - pymdownx.highlight: anchor_linenums: true - pymdownx.superfences - pymdownx.snippets: check_paths: true - pymdownx.tasklist: custom_checkbox: true - sane_lists - smarty - toc: permalink: true validation: links: not_found: warn anchors: warn # builds only if validation succeeds while # threating warnings as errors # also checks for broken links # strict: true ... ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] version = "0.0.2" name = "context-engineering-template" description = "Assess the effectiveness of agentic AI systems across various use cases focusing on agnostic metrics that measure core agentic capabilities." authors = [ {name = "qte77", email = "qte@77.gh"} ] readme = "README.md" requires-python = "==3.13.*" license = "bsd-3-clause" dependencies = [] [project.urls] Documentation = "https://qte77.github.io/context-engineering-template/" [dependency-groups] dev = [] test = [] docs = [ "griffe>=1.5.1", "mkdocs>=1.6.1", "mkdocs-awesome-pages-plugin>=2.9.3", "mkdocs-gen-files>=0.5.0", "mkdocs-literate-nav>=0.6.1", "mkdocs-material>=9.5.44", "mkdocs-section-index>=0.3.8", "mkdocstrings[python]>=0.27.0", ] [tool.uv] package = true exclude-newer = "2025-07-06T00:00:00Z" [tool.hatch.build.targets.wheel] only-include = ["/README.md"] [tool.hatch.build.targets.sdist] include = ["/README.md", "/Makefile", "/tests"] [tool.ruff] target-version = "py313" src = ["src", "tests"] [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] # ignore = ["E203"] # Whitespace before ':' unfixable = ["B"] select = [ # pycodestyle "E", # Pyflakes "F", # pyupgrade "UP", # isort "I", ] [tool.ruff.lint.isort] known-first-party = ["src", "tests"] [tool.ruff.lint.pydocstyle] convention = "google" [tool.mypy] python_version = "3.13" strict = true disallow_untyped_defs = true disallow_any_generics = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = true warn_unreachable = true show_error_codes = true namespace_packages = true explicit_package_bases = true mypy_path = "src" [tool.pytest.ini_options] addopts = "--strict-markers" # "function", "class", "module", "package", "session" asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] testpaths = ["tests/"] [tool.coverage] [tool.coverage.run] include = [ "tests/**/*.py", ] # omit = [] # branch = true [tool.coverage.report] show_missing = true exclude_lines = [ # 'pragma: no cover', 'raise AssertionError', 'raise NotImplementedError', ] omit = [ 'env/*', 'venv/*', '.venv/*', '*/virtualenv/*', '*/virtualenvs/*', '*/tests/*', ] [tool.bumpversion] current_version = "0.0.2" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] commit = true tag = true allow_dirty = false ignore_missing_version = false sign_tags = false tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" message = "Bump version: {current_version} → {new_version}" commit_args = "" [[tool.bumpversion.files]] filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' [[tool.bumpversion.files]] filename = "src/__init__.py" search = '__version__ = "{current_version}"' replace = '__version__ = "{new_version}"' [[tool.bumpversion.files]] filename = "README.md" search = "version-{current_version}-58f4c2" replace = "version-{new_version}-58f4c2" [[tool.bumpversion.files]] filename = "CHANGELOG.md" search = """ ## [Unreleased] """ replace = """ ## [Unreleased] ## [{new_version}] - {now:%Y-%m-%d} """ ================================================ FILE: uv.lock ================================================ version = 1 revision = 2 requires-python = "==3.13.*" [options] exclude-newer = "2025-07-06T00:00:00Z" [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backrefs" version = "5.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] [[package]] name = "bracex" version = "2.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] [[package]] name = "certifi" version = "2025.6.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[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, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "context-engineering-template" version = "0.0.2" source = { editable = "." } [package.dev-dependencies] docs = [ { name = "griffe" }, { name = "mkdocs" }, { name = "mkdocs-awesome-pages-plugin" }, { name = "mkdocs-gen-files" }, { name = "mkdocs-literate-nav" }, { name = "mkdocs-material" }, { name = "mkdocs-section-index" }, { name = "mkdocstrings", extra = ["python"] }, ] [package.metadata] [package.metadata.requires-dev] dev = [] docs = [ { name = "griffe", specifier = ">=1.5.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-awesome-pages-plugin", specifier = ">=2.9.3" }, { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, { name = "mkdocs-literate-nav", specifier = ">=0.6.1" }, { name = "mkdocs-material", specifier = ">=9.5.44" }, { name = "mkdocs-section-index", specifier = ">=0.3.8" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0" }, ] test = [] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "griffe" version = "1.7.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, ] [[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, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "markdown" version = "3.8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, ] [[package]] name = "mkdocs-awesome-pages-plugin" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "natsort" }, { name = "wcmatch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/e8/6ae9c18d8174a5d74ce4ade7a7f4c350955063968bc41ff1e5833cff4a2b/mkdocs_awesome_pages_plugin-2.10.1.tar.gz", hash = "sha256:cda2cb88c937ada81a4785225f20ef77ce532762f4500120b67a1433c1cdbb2f", size = 16303, upload-time = "2024-12-22T21:13:49.19Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/61/19fc1e9c579dbfd4e8a402748f1d63cab7aabe8f8d91eb0235e45b32d040/mkdocs_awesome_pages_plugin-2.10.1-py3-none-any.whl", hash = "sha256:c6939dbea37383fc3cf8c0a4e892144ec3d2f8a585e16fdc966b34e7c97042a7", size = 15118, upload-time = "2024-12-22T21:13:46.945Z" }, ] [[package]] name = "mkdocs-gen-files" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/85/2d634462fd59136197d3126ca431ffb666f412e3db38fd5ce3a60566303e/mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc", size = 7539, upload-time = "2023-04-27T19:48:04.894Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/0f/1e55b3fd490ad2cecb6e7b31892d27cb9fc4218ec1dab780440ba8579e74/mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea", size = 8380, upload-time = "2023-04-27T19:48:07.059Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] name = "mkdocs-literate-nav" version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" }, ] [[package]] name = "mkdocs-material" version = "9.6.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocs-section-index" version = "0.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/40/4aa9d3cfa2ac6528b91048847a35f005b97ec293204c02b179762a85b7f2/mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", size = 14446, upload-time = "2025-04-05T20:56:45.387Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/53/76c109e6f822a6d19befb0450c87330b9a6ce52353de6a9dda7892060a1f/mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776", size = 8796, upload-time = "2025-04-05T20:56:43.975Z" }, ] [[package]] name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, ] [package.optional-dependencies] python = [ { name = "mkdocstrings-python" }, ] [[package]] name = "mkdocstrings-python" version = "1.16.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, ] [[package]] name = "natsort" version = "8.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pymdown-extensions" version = "10.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "requests" version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wcmatch" version = "10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, ] ================================================ FILE: .env.example ================================================ ANTHROPIC_API_KEY="sk-abc-xyz" GEMINI_API_KEY="xyz" GITHUB_API_KEY="ghp_xyz" GROK_API_KEY="xai-xyz" HUGGINGFACE_API_KEY="hf_xyz" OPENROUTER_API_KEY="sk-or-v1-xyz" PERPLEXITY_API_KEY="" RESTACK_API_KEY="xyz" TOGETHER_API_KEY="xyz" ================================================ FILE: .gitmessage ================================================ #<--- 72 characters ---------------------------------------------------> # # Conventional Commits, semantic commit messages for humans and machines # https://www.conventionalcommits.org/en/v1.0.0/ # Lint your conventional commits # https://github.com/conventional-changelog/commitlint/tree/master/%40 \ # commitlint/config-conventional # Common types can be (based on Angular convention) # build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test # https://github.com/conventional-changelog/commitlint/tree/master/%40 # Footer # https://git-scm.com/docs/git-interpret-trailers # #<--- pattern ---------------------------------------------------------> # # [(Scope)][!]: \ # # short description: [()]: # # ! after scope in header indicates breaking change # # [optional body] # # - with bullets points # # [optional footer(s)] # # [BREAKING CHANGE:, Refs:, Resolves:, Addresses:, Reviewed by:] # #<--- usage -----------------------------------------------------------> # # Set locally (in the repository) # `git config commit.template .gitmessage` # # Set globally # `git config --global commit.template .gitmessage` # #<--- 72 characters ---------------------------------------------------> ================================================ FILE: assets/images/example_execute_feature_mcp_client_created.PNG ================================================ [Non-text file] ================================================ FILE: assets/images/example_execute_feature_mcp_server_created.PNG ================================================ [Non-text file] ================================================ FILE: assets/images/example_execute_feature_mcp_server_PRP_update_pyproject.PNG ================================================ [Non-text file] ================================================ FILE: assets/images/example_execute_feature_mcp_server_PRP_update_pyproject_diff.PNG ================================================ [Non-text file] ================================================ FILE: assets/images/example_generate_feature_mcp_server_PRP.PNG ================================================ [Non-text file] ================================================ FILE: context/templates/feature_base.md ================================================ # Feature description for: [ Initial template for new features ] ## FEATURE [Insert your feature here] ## EXAMPLES [Provide and explain examples that you have in the `/context/examples` folder] ## DOCUMENTATION [List out any documentation (web pages, sources for an MCP server like Crawl4AI RAG, etc.) that will need to be referenced during development] ## OTHER CONSIDERATIONS [Any other considerations or specific requirements - great place to include gotchas that you see AI coding assistants miss with your projects a lot] ================================================ FILE: context/templates/prp_base.md ================================================ # "Base PRP Template v2 - Context-Rich with Validation Loops" ## Purpose Product Requirements Prompt (PRP) Template optimized for AI agents to implement features with sufficient context and self-validation capabilities to achieve working code through iterative refinement. ## Core Principles 1. **Context is King**: Include ALL necessary documentation, examples, and caveats 2. **Validation Loops**: Provide executable tests/lints the AI can run and fix 3. **Information Dense**: Use keywords and patterns from the codebase 4. **Progressive Success**: Start simple, validate, then enhance 5. **Global rules**: Be sure to follow all rules in CLAUDE.md --- ## Goal [What needs to be built - be specific about the end state and desires] ## Why - [Business value and user impact] - [Integration with existing features] - [Problems this solves and for whom] ## What [User-visible behavior and technical requirements] ### Success Criteria - [ ] [Specific measurable outcomes] ## All Needed Context ### Documentation & References (list all context needed to implement the feature) ```yaml # MUST READ - Include these in your context window - url: [Official API docs URL] why: [Specific sections/methods you'll need] - file: [path/to/example.py] why: [Pattern to follow, gotchas to avoid] - doc: [Library documentation URL] section: [Specific section about common pitfalls] critical: [Key insight that prevents common errors] - docfile: [PRPs/ai_docs/file.md] why: [docs that the user has pasted in to the project] ``` ### Current Codebase tree (run `tree` in the root of the project) to get an overview of the codebase ```bash ``` ### Desired Codebase tree with files to be added and responsibility of file ```bash ``` ### Known Gotchas of our codebase & Library Quirks ```python # CRITICAL: [Library name] requires [specific setup] # Example: FastAPI requires async functions for endpoints # Example: This ORM doesn't support batch inserts over 1000 records # Example: We use pydantic v2 and ``` ## Implementation Blueprint ### Data models and structure Create the core data models, we ensure type safety and consistency. ```python Examples: - orm models - pydantic models - pydantic schemas - pydantic validators ``` ### list of tasks to be completed to fullfill the PRP in the order they should be completed ```yaml Task 1: MODIFY src/existing_module.py: - FIND pattern: "class OldImplementation" - INJECT after line containing "def __init__" - PRESERVE existing method signatures CREATE src/new_feature.py: - MIRROR pattern from: src/similar_feature.py - MODIFY class name and core logic - KEEP error handling pattern identical ...(...) Task N: ... ``` ### Per task pseudocode as needed added to each task ```python # Task 1 # Pseudocode with CRITICAL details dont write entire code async def new_feature(param: str) -> Result: # PATTERN: Always validate input first (see src/validators.py) validated = validate_input(param) # raises ValidationError # GOTCHA: This library requires connection pooling async with get_connection() as conn: # see src/db/pool.py # PATTERN: Use existing retry decorator @retry(attempts=3, backoff=exponential) async def _inner(): # CRITICAL: API returns 429 if >10 req/sec await rate_limiter.acquire() return await external_api.call(validated) result = await _inner() # PATTERN: Standardized response format return format_response(result) # see src/utils/responses.py ``` ### Integration Points ```yaml DATABASE: - migration: "Add column 'feature_enabled' to users table" - index: "CREATE INDEX idx_feature_lookup ON users(feature_id)" CONFIG: - add to: config/settings.py - pattern: "FEATURE_TIMEOUT = int(os.getenv('FEATURE_TIMEOUT', '30'))" ROUTES: - add to: src/api/routes.py - pattern: "router.include_router(feature_router, prefix='/feature')" ``` ## Validation Loop ### Level 1: Syntax & Style ```bash # Run these FIRST - fix any errors before proceeding ruff check src/new_feature.py --fix # Auto-fix what's possible mypy src/new_feature.py # Type checking # Expected: No errors. If errors, READ the error and fix. ``` ### Level 2: Unit Tests each new feature/file/function use existing test patterns ```python # CREATE test_new_feature.py with these test cases: def test_happy_path(): """Basic functionality works""" result = new_feature("valid_input") assert result.status == "success" def test_validation_error(): """Invalid input raises ValidationError""" with pytest.raises(ValidationError): new_feature("") def test_external_api_timeout(): """Handles timeouts gracefully""" with mock.patch('external_api.call', side_effect=TimeoutError): result = new_feature("valid") assert result.status == "error" assert "timeout" in result.message ``` ```bash # Run and iterate until passing: uv run pytest test_new_feature.py -v # If failing: Read error, understand root cause, fix code, re-run (never mock to pass) ``` ### Level 3: Integration Test ```bash # Start the service uv run python -m src.main --dev # Test the endpoint curl -X POST http://localhost:8000/feature \ -H "Content-Type: application/json" \ -d '{"param": "test_value"}' # Expected: {"status": "success", "data": {...}} # If error: Check logs at logs/app.log for stack trace ``` ## Final validation Checklist - [ ] All tests pass: `uv run pytest tests/ -v` - [ ] No linting errors: `uv run ruff check src/` - [ ] No type errors: `uv run mypy src/` - [ ] Manual test successful: [specific curl/command] - [ ] Error cases handled gracefully - [ ] Logs are informative but not verbose - [ ] Documentation updated if needed --- ## Anti-Patterns to Avoid - ❌ Don't create new patterns when existing ones work - ❌ Don't skip validation because "it should work" - ❌ Don't ignore failing tests - fix them - ❌ Don't use sync functions in async context - ❌ Don't hardcode values that should be config - ❌ Don't catch all exceptions - be specific ================================================ FILE: docs/llms.txt ================================================ ├── .claude ├── commands │ ├── execute-prp.md │ └── generate-prp.md └── settings.local.json ├── .devcontainer └── setup_python_claude │ └── devcontainer.json ├── .env.example ├── .github ├── dependabot.yaml ├── scripts │ ├── create_pr.sh │ └── delete_branch_pr_tag.sh └── workflows │ ├── bump-my-version.yaml │ ├── codeql.yaml │ ├── generate-deploy-mkdocs-ghpages.yaml │ ├── links-fail-fast.yaml │ ├── pytest.yaml │ ├── ruff.yaml │ ├── summarize-jobs-reusable.yaml │ └── write-llms-txt.yaml ├── .gitignore ├── .gitmessage ├── .vscode ├── extensions.json └── settings.json ├── AGENTS.md ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── Makefile ├── README.md ├── assets └── images │ ├── example_execute_feature_mcp_server_PRP_update_pyproject.PNG │ ├── example_execute_feature_mcp_server_PRP_update_pyproject_diff.PNG │ ├── example_execute_feature_mcp_server_created.PNG │ ├── example_generate_feature_mcp_server_PRP.PNG │ ├── sequence_diagram.png │ └── sequence_diagram.svg ├── context └── templates │ ├── feature_base.md │ └── prp_base.md ├── docs ├── architecture │ └── sequence_diagram.mermaid └── llms.txt ├── examples └── mcp-server-client │ ├── Makefile │ ├── context │ ├── PRPs │ │ ├── feature_1_mcp_server.md │ │ ├── feature_2_mcp_client.md │ │ └── feature_3_streamlit_gui.md │ ├── features │ │ ├── feature_1_mcp_server.md │ │ ├── feature_2_mcp_client.md │ │ └── feature_3_streamlit_gui.md │ ├── outputs │ │ ├── client_get_date_input.json │ │ ├── client_get_weather_input.json │ │ ├── client_invalid_tool.json │ │ ├── client_roll_dice_input.json │ │ ├── get_date_example.json │ │ ├── get_weather_example.json │ │ ├── roll_dice_example.json │ │ ├── streamlit_error_handling.json │ │ ├── streamlit_get_date_interaction.json │ │ ├── streamlit_get_weather_interaction.json │ │ └── streamlit_roll_dice_interaction.json │ └── templates │ │ ├── feature_base.md │ │ └── prp_base.md │ ├── pyproject.toml │ ├── src │ ├── __init__.py │ ├── gui │ │ ├── __init__.py │ │ ├── app.py │ │ ├── components │ │ │ ├── __init__.py │ │ │ ├── connection.py │ │ │ ├── history.py │ │ │ └── tool_forms.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── gui_models.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── async_helper.py │ │ │ ├── formatting.py │ │ │ ├── mcp_wrapper.py │ │ │ └── validation.py │ ├── main.py │ ├── mcp_client │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── client.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── responses.py │ │ └── transport.py │ ├── mcp_server │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── requests.py │ │ ├── server.py │ │ └── tools │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── date_time.py │ │ │ ├── dice.py │ │ │ └── weather.py │ └── py.typed │ ├── tests │ ├── __init__.py │ ├── fixtures │ │ ├── __init__.py │ │ └── mcp_messages.py │ ├── test_cli.py │ ├── test_gui.py │ ├── test_mcp_client.py │ ├── test_mcp_server.py │ └── test_tools │ │ ├── __init__.py │ │ ├── test_date_time.py │ │ ├── test_dice.py │ │ └── test_weather.py │ └── uv.lock ├── mkdocs.yaml ├── pyproject.toml ├── src ├── __init__.py ├── main.py └── py.typed └── uv.lock /.claude/commands/execute-prp.md: -------------------------------------------------------------------------------- 1 | # Execute Product Requirements Prompt (PRP) 2 | 3 | Implement a feature using using the PRP file. 4 | 5 | ## PRP File: $ARGUMENTS 6 | 7 | ## Execution Process 8 | 9 | 1. **Load PRP** 10 | - Read the specified PRP file 11 | - Understand all context and requirements 12 | - Follow all instructions in the PRP and extend the research if needed 13 | - Ensure you have all needed context to implement the PRP fully 14 | - Do more web searches and codebase exploration as needed 15 | 16 | 2. **ULTRATHINK** 17 | - Think hard before you execute the plan. Create a comprehensive plan addressing all requirements. 18 | - Break down complex tasks into smaller, manageable steps using your todos tools. 19 | - Use the TodoWrite tool to create and track your implementation plan. 20 | - Identify implementation patterns from existing code to follow. 21 | 22 | 3. **Execute the plan** 23 | - Execute the PRP 24 | - Implement all the code 25 | 26 | 4. **Validate** 27 | - Run each validation command 28 | - Fix any failures 29 | - Re-run until all pass 30 | 31 | 5. **Complete** 32 | - Ensure all checklist items done 33 | - Run final validation suite 34 | - Report completion status 35 | - Read the PRP again to ensure you have implemented everything 36 | 37 | 6. **Reference the PRP** 38 | - You can always reference the PRP again if needed 39 | 40 | Note: If validation fails, use error patterns in PRP to fix and retry. 41 | -------------------------------------------------------------------------------- /.claude/commands/generate-prp.md: -------------------------------------------------------------------------------- 1 | # Create Product Requirements Prompt (PRP) 2 | 3 | ## Feature file: $ARGUMENTS 4 | 5 | Generate a complete PRP (Product Requirements Prompt) for general feature implementation with thorough research. Ensure context is passed to the AI agent to enable self-validation and iterative refinement. Read the feature file first to understand what needs to be created, how the examples provided help, and any other considerations. 6 | 7 | The AI agent only gets the context you are appending to the PRP and training data. Assume the AI agent has access to the codebase and the same knowledge cutoff as you, so its important that your research findings are included or referenced in the PRP. The Agent has Websearch capabilities, so pass urls to documentation and examples. 8 | 9 | - Use `/context/PRPs` as `$base_path` 10 | - Extract only the filename from `$ARGUMENTS` into `$file_name` 11 | 12 | ## Research Process 13 | 14 | 1. **Codebase Analysis** 15 | - Search for similar features/patterns in the codebase 16 | - Identify files to reference in PRP 17 | - Note existing conventions to follow 18 | - Check test patterns for validation approach 19 | 20 | 2. **External Research** 21 | - Search for similar features/patterns online 22 | - Library documentation (include specific URLs) 23 | - Implementation examples (GitHub/StackOverflow/blogs) 24 | - Best practices and common pitfalls 25 | 26 | 3. **User Clarification** (if needed) 27 | - Specific patterns to mirror and where to find them? 28 | - Integration requirements and where to find them? 29 | 30 | ## PRP Generation 31 | 32 | - Use `${base_path}/templates/prp_base.md` in the base folder as template 33 | 34 | ### Critical Context to Include and pass to the AI agent as part of the PRP 35 | 36 | - **Documentation**: URLs with specific sections 37 | - **Code Examples**: Real snippets from codebase 38 | - **Gotchas**: Library quirks, version issues 39 | - **Patterns**: Existing approaches to follow 40 | 41 | ### Implementation Blueprint 42 | 43 | - Start with pseudocode showing approach 44 | - Reference real files for patterns 45 | - Include error handling strategy 46 | - list tasks to be completed to fullfill the PRP in the order they should be completed 47 | 48 | ### Validation Gates (Must be Executable) eg for python 49 | 50 | ```bash 51 | # Syntax/Style 52 | make ruff 53 | make check_types 54 | 55 | # Unit Tests 56 | make coverage_all 57 | ``` 58 | 59 | ***CRITICAL AFTER YOU ARE DONE RESEARCHING AND EXPLORING THE CODEBASE BEFORE YOU START WRITING THE PRP*** 60 | 61 | ***ULTRATHINK ABOUT THE PRP AND PLAN YOUR APPROACH THEN START WRITING THE PRP*** 62 | 63 | ## Output 64 | 65 | - Save the result to `${base_path}/${file_name}` 66 | 67 | ## Quality Checklist 68 | 69 | - [ ] All necessary context included 70 | - [ ] Validation gates are executable by AI 71 | - [ ] References existing patterns 72 | - [ ] Clear implementation path 73 | - [ ] Error handling documented 74 | 75 | Score the PRP on a scale of 1-10 (confidence level to succeed in one-pass implementation using claude codes) 76 | 77 | Remember: The goal is one-pass implementation success through comprehensive context. 78 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(cat:*)", 5 | "Bash(find:*)", 6 | "Bash(git:diff*)", 7 | "Bash(git:log*)", 8 | "Bash(git:status*)", 9 | "Bash(grep:*)", 10 | "Bash(ls:*)", 11 | "Bash(mkdir:*)", 12 | "Bash(source:*)", 13 | "Bash(touch:*)", 14 | "Bash(tree:*)", 15 | "Bash(uv:run*)", 16 | "Edit(AGENTS.md)", 17 | "Edit(docs/**/*.md)", 18 | "Edit(src/**/*.py)", 19 | "Edit(src/**/*.json)", 20 | "Edit(tests/**/*.py)", 21 | "WebFetch(domain:docs.anthropic.com)" 22 | ], 23 | "deny": [ 24 | "Bash(mv:*)", 25 | "Bash(rm:*)", 26 | "Edit(CLAUDE.md" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /.devcontainer/setup_python_claude/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup_python_claude", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": {} 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "anthropic.claude-code" 11 | ] 12 | } 13 | }, 14 | "postCreateCommand": "make setup_python_claude" 15 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ANTHROPIC_API_KEY="sk-abc-xyz" 2 | GEMINI_API_KEY="xyz" 3 | GITHUB_API_KEY="ghp_xyz" 4 | GROK_API_KEY="xai-xyz" 5 | HUGGINGFACE_API_KEY="hf_xyz" 6 | OPENROUTER_API_KEY="sk-or-v1-xyz" 7 | PERPLEXITY_API_KEY="" 8 | RESTACK_API_KEY="xyz" 9 | TOGETHER_API_KEY="xyz" -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "pip" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | ... 10 | -------------------------------------------------------------------------------- /.github/scripts/create_pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 1 base ref, 2 target ref, 3 title suffix 3 | # 4 current version, 5 bumped 4 | 5 | pr_title="PR $2 $3" 6 | pr_body="PR automatically created from \`$1\` to bump from \`$4\` to \`$5\` on \`$2\`. Tag \`v$5\` will be created and has to be deleted manually if PR gets closed without merge." 7 | 8 | gh pr create \ 9 | --base $1 \ 10 | --head $2 \ 11 | --title "${pr_title}" \ 12 | --body "${pr_body}" 13 | # --label "bump" 14 | -------------------------------------------------------------------------------- /.github/scripts/delete_branch_pr_tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 1 repo, 2 target ref, 3 current version 3 | 4 | tag_to_delete="v$3" 5 | branch_del_api_call="repos/$1/git/refs/heads/$2" 6 | del_msg="'$2' force deletion attempted." 7 | close_msg="Closing PR '$2' to rollback after failure" 8 | 9 | echo "Tag $tag_to_delete for $del_msg" 10 | git tag -d "$tag_to_delete" 11 | echo "PR for $del_msg" 12 | gh pr close "$2" --comment "$close_msg" 13 | echo "Branch $del_msg" 14 | gh api "$branch_del_api_call" -X DELETE && \ 15 | echo "Branch without error return deleted." -------------------------------------------------------------------------------- /.github/workflows/bump-my-version.yaml: -------------------------------------------------------------------------------- 1 | name: bump-my-version 2 | 3 | on: 4 | # pull_request: 5 | # types: [closed] 6 | # branches: [main] 7 | workflow_dispatch: 8 | inputs: 9 | bump_type: 10 | description: '[major|minor|patch]' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - 'major' 16 | - 'minor' 17 | - 'patch' 18 | 19 | env: 20 | BRANCH_NEW: "bump-${{ github.run_number }}-${{ github.ref_name }}" 21 | SKIP_PR_HINT: "[skip ci bump]" 22 | SCRIPT_PATH: ".github/scripts" 23 | 24 | jobs: 25 | bump_my_version: 26 | # TODO bug? currently resulting in: Unrecognized named-value: 'env'. 27 | # https://stackoverflow.com/questions/61238849/github-actions-if-contains-function-not-working-with-env-variable/61240761 28 | # if: !contains( 29 | # github.event.pull_request.title, 30 | # ${{ env.SKIP_PR_HINT }} 31 | # ) 32 | # TODO check for PR closed by bot to avoid PR creation loop 33 | # github.actor != 'github-actions' 34 | if: > 35 | github.event_name == 'workflow_dispatch' || 36 | ( github.event.pull_request.merged == true && 37 | github.event.pull_request.closed_by != 'github-actions' ) 38 | runs-on: ubuntu-latest 39 | outputs: 40 | branch_new: ${{ steps.create_branch.outputs.branch_new }} 41 | summary_data: ${{ steps.set_summary.outputs.summary_data }} 42 | permissions: 43 | actions: read 44 | checks: write 45 | contents: write 46 | pull-requests: write 47 | steps: 48 | 49 | - name: Checkout repo 50 | uses: actions/checkout@v4 51 | with: 52 | fetch-depth: 1 53 | 54 | - name: Set git cfg and create branch 55 | id: create_branch 56 | run: | 57 | git config user.email "github-actions@users.noreply.github.com" 58 | git config user.name "github-actions[bot]" 59 | git checkout -b "${{ env.BRANCH_NEW }}" 60 | echo "branch_new=${{ env.BRANCH_NEW }}" >> $GITHUB_OUTPUT 61 | 62 | - name: Bump version 63 | id: bump 64 | uses: callowayproject/bump-my-version@0.29.0 65 | env: 66 | BUMPVERSION_TAG: "true" 67 | with: 68 | args: ${{ inputs.bump_type }} 69 | branch: ${{ env.BRANCH_NEW }} 70 | 71 | - name: "Create PR '${{ env.BRANCH_NEW }}'" 72 | if: steps.bump.outputs.bumped == 'true' 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: | 76 | src="${{ env.SCRIPT_PATH }}/create_pr.sh" 77 | chmod +x "$src" 78 | $src "${{ github.ref_name }}" "${{ env.BRANCH_NEW }}" "${{ env.SKIP_PR_HINT }}" "${{ steps.bump.outputs.previous-version }}" "${{ steps.bump.outputs.current-version }}" 79 | 80 | - name: Delete branch, PR and tag in case of failure or cancel 81 | if: failure() || cancelled() 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | run: | 85 | src="${{ env.SCRIPT_PATH }}/delete_branch_pr_tag.sh" 86 | chmod +x "$src" 87 | $src "${{ github.repository }}" "${{ env.BRANCH_NEW }}" "${{ steps.bump.outputs.current-version }}" 88 | 89 | - name: Set summary data 90 | id: set_summary 91 | if: ${{ always() }} 92 | run: echo "summary_data=${GITHUB_STEP_SUMMARY}" >> $GITHUB_OUTPUT 93 | 94 | generate_summary: 95 | name: Generate Summary Report 96 | if: ${{ always() }} 97 | needs: bump_my_version 98 | uses: ./.github/workflows/summarize-jobs-reusable.yaml 99 | with: 100 | branch_to_summarize: ${{ needs.bump_my_version.outputs.branch_new }} 101 | summary_data: ${{ needs.bump_my_version.outputs.summary_data }} 102 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.blog/changelog/2023-01-18-code-scanning-codeql-action-v1-is-now-deprecated/ 3 | name: "CodeQL" 4 | 5 | on: 6 | push: 7 | pull_request: 8 | types: [closed] 9 | branches: [ main ] 10 | schedule: 11 | - cron: '27 11 * * 0' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: python 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v3 34 | # if autobuild fails 35 | #- run: | 36 | # make bootstrap 37 | # make release 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | #- name: sarif 42 | # uses: github/codeql-action/upload-sarif@v2 43 | ... 44 | -------------------------------------------------------------------------------- /.github/workflows/generate-deploy-mkdocs-ghpages.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy Docs 3 | 4 | on: 5 | pull_request: 6 | types: [closed] 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | env: 11 | DOCSTRINGS_FILE: "docstrings.md" 12 | DOC_DIR: "docs" 13 | SRC_DIR: "src" 14 | SITE_DIR: "site" 15 | IMG_DIR: "assets/images" 16 | 17 | jobs: 18 | build-and-deploy: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | pages: write 23 | id-token: write 24 | environment: 25 | name: github-pages 26 | steps: 27 | 28 | - name: Checkout the repository 29 | uses: actions/checkout@v4.0.0 30 | with: 31 | ref: 32 | ${{ 33 | github.event.pull_request.merged == true && 34 | 'main' || 35 | github.ref_name 36 | }} 37 | fetch-depth: 0 38 | 39 | - uses: actions/configure-pages@v5.0.0 40 | 41 | # caching instead of actions/cache@v4.0.0 42 | # https://docs.astral.sh/uv/guides/integration/github/#caching 43 | - name: Install uv with cache dependency glob 44 | uses: astral-sh/setup-uv@v5.0.0 45 | with: 46 | enable-cache: true 47 | cache-dependency-glob: "uv.lock" 48 | 49 | # setup python from pyproject.toml using uv 50 | # instead of using actions/setup-python@v5.0.0 51 | # https://docs.astral.sh/uv/guides/integration/github/#setting-up-python 52 | - name: "Set up Python" 53 | run: uv python install 54 | 55 | - name: Install only doc deps 56 | run: uv sync --only-group docs # --frozen 57 | 58 | - name: Get repo info and stream into mkdocs.yaml 59 | id: repo_info 60 | run: | 61 | REPO_INFO=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 62 | -H "Accept: application/vnd.github.v3+json" \ 63 | https://api.github.com/repos/${{ github.repository }}) 64 | REPO_URL="${{ github.server_url }}/${{ github.repository }}" 65 | REPO_URL=$(echo ${REPO_URL} | sed 's|/|\\/|g') 66 | SITE_NAME=$(sed '1!d' README.md | sed '0,/# /{s/# //}') 67 | SITE_DESC=$(echo $REPO_INFO | jq -r .description) 68 | sed -i "s//${REPO_URL}/g" mkdocs.yaml 69 | sed -i "s//${SITE_NAME}/g" mkdocs.yaml 70 | sed -i "s//${SITE_DESC}/g" mkdocs.yaml 71 | 72 | - name: Copy text files to be included 73 | run: | 74 | CFG_PATH="src/app/config" 75 | mkdir -p "${DOC_DIR}/${CFG_PATH}" 76 | cp README.md "${DOC_DIR}/index.md" 77 | cp CHANGELOG.md LICENSE "${DOC_DIR}" 78 | # Auxiliary files 79 | cp .env.example "${DOC_DIR}" 80 | 81 | - name: Generate code docstrings concat file 82 | run: | 83 | PREFIX="::: " 84 | find "${SRC_DIR}" -type f -name "*.py" \ 85 | -type f -not -name "__*__*" -printf "%P\n" | \ 86 | sed 's/\//./g' | sed 's/\.py$//' | \ 87 | sed "s/^/${PREFIX}/" | sort > \ 88 | "${DOC_DIR}/${DOCSTRINGS_FILE}" 89 | 90 | - name: Build documentation 91 | run: uv run --locked --only-group docs mkdocs build 92 | 93 | - name: Copy image files to be included 94 | run: | 95 | # copy images, mkdocs does not by default 96 | # mkdocs also overwrites pre-made directories 97 | dir="${{ env.SITE_DIR }}/${{ env.IMG_DIR }}" 98 | if [ -d "${{ env.IMG_DIR }}" ]; then 99 | mkdir -p "${dir}" 100 | cp "${{ env.IMG_DIR }}"/* "${dir}" 101 | fi 102 | 103 | # - name: Push to gh-pages 104 | # run: uv run mkdocs gh-deploy --force 105 | 106 | - name: Upload artifact 107 | uses: actions/upload-pages-artifact@v3.0.0 108 | with: 109 | path: "${{ env.SITE_DIR }}" 110 | 111 | - name: Deploy to GitHub Pages 112 | id: deployment 113 | uses: actions/deploy-pages@v4.0.0 114 | ... 115 | -------------------------------------------------------------------------------- /.github/workflows/links-fail-fast.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.com/lycheeverse/lychee-action 3 | # https://github.com/marketplace/actions/lychee-broken-link-checker 4 | name: "Link Checker" 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches-ignore: [main] 10 | pull_request: 11 | types: [closed] 12 | branches: [main] 13 | schedule: 14 | - cron: "00 00 * * 0" 15 | 16 | jobs: 17 | linkChecker: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | issues: write 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Link Checker 26 | id: lychee 27 | uses: lycheeverse/lychee-action@v2 28 | 29 | - name: Create Issue From File 30 | if: steps.lychee.outputs.exit_code != 0 31 | uses: peter-evans/create-issue-from-file@v5 32 | with: 33 | title: lychee Link Checker Report 34 | content-filepath: ./lychee/out.md 35 | labels: report, automated issue 36 | ... 37 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.12' 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pytest 22 | 23 | - name: Run tests 24 | run: pytest 25 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.com/astral-sh/ruff-action 3 | # https://github.com/astral-sh/ruff 4 | name: ruff 5 | on: 6 | push: 7 | pull_request: 8 | types: [closed] 9 | branches: [main] 10 | schedule: 11 | - cron: "0 0 * * 0" 12 | workflow_dispatch: 13 | jobs: 14 | ruff: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: astral-sh/ruff-action@v3 19 | ... 20 | -------------------------------------------------------------------------------- /.github/workflows/summarize-jobs-reusable.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://ecanarys.com/supercharging-github-actions-with-job-summaries-and-pull-request-comments/ 3 | # FIXME currently bug in gha summaries ? $GITHUB_STEP_SUMMARY files are empty 4 | # https://github.com/orgs/community/discussions/110283 5 | # https://github.com/orgs/community/discussions/67991 6 | # Possible workaround 7 | # echo ${{ fromJSON(step).name }}" >> $GITHUB_STEP_SUMMARY 8 | # echo ${{ fromJSON(step).outcome }}" >> $GITHUB_STEP_SUMMARY 9 | # echo ${{ fromJSON(step).conclusion }}" 10 | 11 | name: Summarize workflow jobs 12 | 13 | on: 14 | workflow_call: 15 | outputs: 16 | summary: 17 | description: "Outputs summaries of jobs in a workflow" 18 | value: ${{ jobs.generate_summary.outputs.summary }} 19 | inputs: 20 | branch_to_summarize: 21 | required: false 22 | default: 'main' 23 | type: string 24 | summary_data: 25 | required: false 26 | type: string 27 | 28 | jobs: 29 | generate_summary: 30 | name: Generate Summary 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: read 34 | actions: read 35 | checks: read 36 | pull-requests: none 37 | outputs: 38 | summary: ${{ steps.add_changed_files.outputs.summary }} 39 | steps: 40 | 41 | - name: Add general information 42 | id: general_info 43 | run: | 44 | echo "# Job Summaries" >> $GITHUB_STEP_SUMMARY 45 | echo "Job: `${{ github.job }}`" >> $GITHUB_STEP_SUMMARY 46 | echo "Date: $(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_STEP_SUMMARY 47 | 48 | - name: Add step states 49 | id: step_states 50 | run: | 51 | echo "### Steps:" >> $GITHUB_STEP_SUMMARY 52 | # loop summary_data if valid json 53 | if jq -e . >/dev/null 2>&1 <<< "${{ inputs.summary_data }}"; then 54 | jq -r ' 55 | .steps[] 56 | | select(.conclusion != null) 57 | | "- **\(.name)**: \( 58 | if .conclusion == "success" then ":white_check_mark:" 59 | elif .conclusion == "failure" then ":x:" 60 | else ":warning:" end 61 | )" 62 | ' <<< "${{ inputs.summary_data }}" >> $GITHUB_STEP_SUMMARY 63 | else 64 | echo "Invalid JSON in summary data." >> $GITHUB_STEP_SUMMARY 65 | fi 66 | 67 | - name: Checkout repo 68 | uses: actions/checkout@v4 69 | with: 70 | ref: "${{ inputs.branch_to_summarize }}" 71 | fetch-depth: 0 72 | 73 | - name: Add changed files since last push 74 | id: add_changed_files 75 | run: | 76 | # Get the tags 77 | # Use disabled lines to get last two commits 78 | # current=$(git show -s --format=%ci HEAD) 79 | # previous=$(git show -s --format=%ci HEAD~1) 80 | # git diff --name-only HEAD^ HEAD >> $GITHUB_STEP_SUMMARY 81 | version_tag_regex="^v[0-9]+\.[0-9]+\.[0-9]+$" # v0.0.0 82 | tags=$(git tag --sort=-version:refname | \ 83 | grep -E "${version_tag_regex}" || echo "") 84 | 85 | # Get latest and previous tags 86 | latest_tag=$(echo "${tags}" | head -n 1) 87 | previous_tag=$(echo "${tags}" | head -n 2 | tail -n 1) 88 | 89 | echo "tags: latest '${latest_tag}', previous '${previous_tag}'" 90 | 91 | # Write to summary 92 | error_msg="No files to output. Tag not found:" 93 | echo ${{ steps.step_states.outputs.summary }} >> $GITHUB_STEP_SUMMARY 94 | echo "## Changed files on '${{ inputs.branch_to_summarize }}'" >> $GITHUB_STEP_SUMMARY 95 | 96 | if [ -z "${latest_tag}" ]; then 97 | echo "${error_msg} latest" >> $GITHUB_STEP_SUMMARY 98 | elif [ -z "${previous_tag}" ]; then 99 | echo "${error_msg} previous" >> $GITHUB_STEP_SUMMARY 100 | elif [ "${latest_tag}" == "${previous_tag}" ]; then 101 | echo "Latest and previous tags are the same: '${latest_tag}'" >> $GITHUB_STEP_SUMMARY 102 | else 103 | # Get commit dates and hashes 104 | latest_date=$(git log -1 --format=%ci $latest_tag) 105 | previous_date=$(git log -1 --format=%ci $previous_tag) 106 | current_hash=$(git rev-parse --short $latest_tag) 107 | previous_hash=$(git rev-parse --short $previous_tag) 108 | 109 | # Append summary to the job summary 110 | echo "Latest Tag Commit: '${latest_tag}' (${current_hash}) ${latest_date}" >> $GITHUB_STEP_SUMMARY 111 | echo "Previous Tag Commit: '${previous_tag}' (${previous_hash}) ${previous_date}" >> $GITHUB_STEP_SUMMARY 112 | echo "Files changed:" >> $GITHUB_STEP_SUMMARY 113 | echo '```' >> $GITHUB_STEP_SUMMARY 114 | git diff --name-only $previous_tag..$latest_tag >> $GITHUB_STEP_SUMMARY 115 | echo '```' >> $GITHUB_STEP_SUMMARY 116 | fi 117 | 118 | - name: Output error message in case of failure or cancel 119 | if: failure() || cancelled() 120 | run: | 121 | if [ "${{ job.status }}" == "cancelled" ]; then 122 | out_msg="## Workflow was cancelled" 123 | else 124 | out_msg="## Error in previous step" 125 | fi 126 | echo $out_msg >> $GITHUB_STEP_SUMMARY 127 | ... -------------------------------------------------------------------------------- /.github/workflows/write-llms-txt.yaml: -------------------------------------------------------------------------------- 1 | # TODO use local installation of repo to text 2 | # https://github.com/itsitgroup/repo2txt 3 | name: Write repo llms.txt 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | inputs: 10 | LLMS_TXT_PATH: 11 | description: 'Path to the directory to save llsm.txt' 12 | required: true 13 | default: 'docs' 14 | type: string 15 | LLMS_TXT_NAME: 16 | description: 'Path to the directory to save llsm.txt' 17 | required: true 18 | default: 'llms.txt' 19 | type: string 20 | CONVERTER_URL: 21 | description: '[uithub|gittodoc]' # |repo2txt 22 | required: true 23 | default: 'uithub.com' 24 | type: choice 25 | options: 26 | - 'uithub.com' 27 | - 'gittodoc.com' 28 | # - 'repo2txt.com' 29 | 30 | jobs: 31 | generate-file: 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - name: Checkout repo 36 | uses: actions/checkout@v4 37 | 38 | - name: Construct and create llms.txt path 39 | id: construct_and_create_llms_txt_path 40 | run: | 41 | LLMS_TXT_PATH="${{ inputs.LLMS_TXT_PATH }}" 42 | LLMS_TXT_PATH="${LLMS_TXT_PATH:-docs}" 43 | LLMS_TXT_NAME="${{ inputs.LLMS_TXT_NAME }}" 44 | LLMS_TXT_NAME="${LLMS_TXT_NAME:-llms.txt}" 45 | echo "LLMS_TXT_FULL=${LLMS_TXT_PATH}/${LLMS_TXT_NAME}" >> $GITHUB_OUTPUT 46 | mkdir -p "${LLMS_TXT_PATH}" 47 | 48 | - name: Fetch TXT from URL 49 | run: | 50 | LLMS_TXT_FULL=${{ steps.construct_and_create_llms_txt_path.outputs.LLMS_TXT_FULL }} 51 | URL="https://${{ inputs.CONVERTER_URL }}/${{ github.repository }}" 52 | echo "Fetching content from: ${URL}" 53 | echo "Saving content to: ${LLMS_TXT_FULL}" 54 | curl -s "${URL}" > "${LLMS_TXT_FULL}" 55 | 56 | - name: Commit and push file 57 | run: | 58 | LLMS_TXT_FULL=${{ steps.construct_and_create_llms_txt_path.outputs.LLMS_TXT_FULL }} 59 | commit_msg="feat(docs): Add/Update ${LLMS_TXT_FULL}, a flattened repo as single text file, inspired by [llmstxt.org](https://llmstxt.org/)." 60 | git config user.name "github-actions" 61 | git config user.email "github-actions@github.com" 62 | git add "${LLMS_TXT_FULL}" 63 | git commit -m "${commit_msg}" 64 | git push 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # environment 6 | .venv/ 7 | *.env 8 | 9 | # Distribution / packaging 10 | build/ 11 | dist/ 12 | *.egg-info/ 13 | 14 | # Testing 15 | .pytest_cache/ 16 | .coverage 17 | 18 | # Logs 19 | *.log 20 | /logs 21 | 22 | # OS generated files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # IDE specific files (adjust as needed) 27 | # .vscode/ 28 | # .idea/ 29 | 30 | # mkdocs 31 | reference/ 32 | site/ 33 | 34 | # linting 35 | .ruff_cache 36 | 37 | # type checking 38 | .mypy_cache/ 39 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | #<--- 72 characters ---------------------------------------------------> 2 | # 3 | # Conventional Commits, semantic commit messages for humans and machines 4 | # https://www.conventionalcommits.org/en/v1.0.0/ 5 | # Lint your conventional commits 6 | # https://github.com/conventional-changelog/commitlint/tree/master/%40 \ 7 | # commitlint/config-conventional 8 | # Common types can be (based on Angular convention) 9 | # build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test 10 | # https://github.com/conventional-changelog/commitlint/tree/master/%40 11 | # Footer 12 | # https://git-scm.com/docs/git-interpret-trailers 13 | # 14 | #<--- pattern ---------------------------------------------------------> 15 | # 16 | # [(Scope)][!]: \ 17 | # 18 | # short description: [()]: 19 | # 20 | # ! after scope in header indicates breaking change 21 | # 22 | # [optional body] 23 | # 24 | # - with bullets points 25 | # 26 | # [optional footer(s)] 27 | # 28 | # [BREAKING CHANGE:, Refs:, Resolves:, Addresses:, Reviewed by:] 29 | # 30 | #<--- usage -----------------------------------------------------------> 31 | # 32 | # Set locally (in the repository) 33 | # `git config commit.template .gitmessage` 34 | # 35 | # Set globally 36 | # `git config --global commit.template .gitmessage` 37 | # 38 | #<--- 72 characters ---------------------------------------------------> -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "charliermarsh.ruff", 4 | "davidanson.vscode-markdownlint", 5 | "donjayamanne.githistory", 6 | "editorconfig.editorconfig", 7 | "gruntfuggly.todo-tree", 8 | "mhutchie.git-graph", 9 | "PKief.material-icon-theme", 10 | "redhat.vscode-yaml", 11 | "tamasfe.even-better-toml", 12 | "yzhang.markdown-all-in-one", 13 | 14 | "github.copilot", 15 | "github.copilot-chat", 16 | "github.vscode-github-actions", 17 | "ms-azuretools.vscode-docker", 18 | "ms-python.debugpy", 19 | "ms-python.python", 20 | "ms-python.vscode-pylance", 21 | "ms-vscode.makefile-tools", 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.lineNumbers": "on", 3 | "editor.wordWrap": "on", 4 | "explorer.confirmDelete": true, 5 | "files.autoSave": "onFocusChange", 6 | "git.autofetch": true, 7 | "git.enableSmartCommit": true, 8 | "makefile.configureOnOpen": false, 9 | "markdownlint.config": { 10 | "MD024": false, 11 | "MD033": false 12 | }, 13 | "python.analysis.extraPaths": ["./venv/lib/python3.13/site-packages"], 14 | "python.defaultInterpreterPath": "./.venv/bin/python", 15 | "python.analysis.typeCheckingMode": "strict", 16 | "python.analysis.diagnosticSeverityOverrides": { 17 | "reportMissingTypeStubs": "none", 18 | "reportUnknownMemberType": "none", 19 | "reportUnknownVariableType": "none" 20 | }, 21 | "redhat.telemetry.enabled": false 22 | } -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Agent instructions for `Agents-eval` repository 2 | 3 | As proposed by [agentsmd.net](https://agentsmd.net/) and used by [wandb weave AGENTS.md](https://github.com/wandb/weave/blob/master/AGENTS.md). 4 | 5 | ## Core Rules & AI Behavior 6 | 7 | * When you learn something new about the codebase or introduce a new concept, **update this file (`AGENTS.md`)** to reflect the new knowledge. This is YOUR FILE! It should grow and evolve with you. 8 | * If something doesn't make sense architecturally, from a developer experience standpoint, or product-wise, please add it to the **`Requests to Humans`** section below. 9 | * Always follow the established coding patterns, conventions, and architectural decisions documented here and in the `docs/` directory. 10 | * **Never assume missing context.** Ask questions if you are uncertain about requirements or implementation details. 11 | * **Never hallucinate libraries or functions.** Only use known, verified Python packages listed in `pyproject.toml`. 12 | * **Always confirm file paths and module names** exist before referencing them in code or tests. 13 | * **Never delete or overwrite existing code** unless explicitly instructed to or as part of a documented refactoring task. 14 | 15 | ## Architecture Overview 16 | 17 | This is a multi-agent evaluation system for assessing agentic AI systems. The project uses **PydanticAI** as the core framework for agent orchestration and is designed for evaluation purposes, not for production agent deployment. 18 | 19 | ### Data Flow 20 | 21 | 1. User input → Manager Agent 22 | 2. Manager delegates to Researcher Agent (with DuckDuckGo search) 23 | 3. Researcher results → Analyst Agent for validation 24 | 4. Validated data → Synthesizer Agent for report generation 25 | 5. Results evaluated using configurable metrics 26 | 27 | ### Key Dependencies 28 | 29 | * **PydanticAI**: Agent framework and orchestration 30 | * **uv**: Fast Python dependency management 31 | * **Streamlit**: GUI framework 32 | * **Ruff**: Code formatting and linting 33 | * **MyPy**: Static type checking 34 | 35 | ## Codebase Structure & Modularity 36 | 37 | ### Main Components 38 | 39 | * `src/app/`: The core application logic. This is where most of your work will be. 40 | * `main.py`: The main entry point for the CLI application. 41 | * `agents/agent_system.py`: Defines the multi-agent system, their interactions, and orchestration. **This is the central logic for agent behavior.** 42 | * `config/data_models.py`: Contains all **Pydantic** models that define the data contracts. This is a critical file for understanding data flow. 43 | * `config/config_chat.json`: Holds provider settings and system prompts for agents. 44 | * `config/config_eval.json`: Defines evaluation metrics and their weights. 45 | * `evals/metrics.py`: Implements the evaluation metrics. 46 | * `src/gui/`: Contains the source code for the Streamlit GUI. 47 | * `docs/`: Contains project documentation, including the Product Requirements Document (`PRD.md`) and the C4 architecture model. 48 | * `tests/`: Contains all tests for the project, written using **pytest**. 49 | 50 | ### Code Organization Rules 51 | 52 | * **Never create a file longer than 500 lines of code.** If a file approaches this limit, refactor by splitting it into smaller, more focused modules or helper files. 53 | * Organize code into clearly separated modules grouped by feature. 54 | * Use clear, consistent, and absolute imports within packages. 55 | 56 | ## Development Commands & Environment 57 | 58 | ### Environment Setup 59 | 60 | The project requirements are stated in `pyproject.toml`. Your development environment should be set up automatically using the provided `Makefile`, which configures the virtual environment. 61 | 62 | * `make setup_dev`: Install all dev dependencies. 63 | * `make setup_dev_claude`: Setup dev environment with Claude Code CLI. 64 | * `make setup_dev_ollama`: Setup dev environment with Ollama local LLM. 65 | 66 | ### Running the Application 67 | 68 | * `make run_cli`: Run the CLI application. 69 | * `make run_cli ARGS="--help"`: Run CLI with specific arguments. 70 | * `make run_gui`: Run the Streamlit GUI. 71 | 72 | ### Testing and Code Quality 73 | 74 | * `make test_all`: Run all tests with pytest. 75 | * `make coverage_all`: Run tests and generate a coverage report. 76 | * `make ruff`: Format code and fix linting issues with Ruff. 77 | * `make type_check`: Run mypy static type checking on `src/app/`. 78 | 79 | ## Testing & Reliability 80 | 81 | * **Always create Pytest unit tests** for new features (functions, classes, etc.). 82 | * Tests must live in the `tests/` folder, mirroring the `src/app` structure. 83 | * After updating any logic, check whether existing unit tests need to be updated. If so, do it. 84 | * For each new feature, include at least: 85 | * 1 test for the expected use case (happy path). 86 | * 1 test for a known edge case. 87 | * 1 test for an expected failure case (e.g., invalid input). 88 | * **To run a specific test file or function, use `uv run pytest` directly:** 89 | * `uv run pytest tests/test_specific_file.py` 90 | * `uv run pytest tests/test_specific_file.py::test_function` 91 | 92 | ## Style, Patterns & Documentation 93 | 94 | ### Coding Style 95 | 96 | * **Use Pydantic** models in `src/app/config/data_models.py` for all data validation and data contracts. **Always use or update these models** when modifying data flows. 97 | * Use the predefined error message functions from `src/app/utils/error_messages.py` for consistency. 98 | * When writing complex logic, **add an inline `# Reason:` comment** explaining the *why*, not just the *what*. 99 | * Comment non-obvious code to ensure it is understandable to a mid-level developer. 100 | 101 | ### Documentation 102 | 103 | * Write **docstrings for every function, class, and method** using the Google style format. This is critical as the documentation site is built automatically from docstrings. 104 | 105 | ```python 106 | def example_function(param1: int) -> str: 107 | """A brief summary of the function. 108 | 109 | Args: 110 | param1 (int): A description of the first parameter. 111 | 112 | Returns: 113 | str: A description of the return value. 114 | """ 115 | return "example" 116 | ``` 117 | 118 | * Update this `AGENTS.md` file when introducing new patterns or concepts. 119 | * Document significant architectural decisions in `docs/ADR.md`. 120 | * Document all significant changes, features, and bug fixes in `docs/CHANGELOG.md`. 121 | 122 | ## Code Review & PR Guidelines 123 | 124 | ### PR Requirements 125 | 126 | * **Title Format**: Commit messages and PR titles must follow the **Conventional Commits** specification, as outlined in the `.gitmessage` template. 127 | * Provide detailed PR summaries including the purpose of the changes and the testing performed. 128 | 129 | ### Pre-commit Checklist 130 | 131 | 1. Run the linter and formatter: `make ruff`. 132 | 2. Ensure all tests pass: `make test_all`. 133 | 3. Ensure static type checks pass: `make type_check`. 134 | 4. Update documentation as described below. 135 | 136 | ## Requests to Humans 137 | 138 | This section contains a list of questions, clarifications, or tasks that AI agents wish to have humans complete or elaborate on. 139 | 140 | * [ ] The `agent_system.py` module has a `NotImplementedError` for streaming with Pydantic model outputs. Please clarify the intended approach for streaming structured data. 141 | * [ ] The `llm_model_funs.py` module has `NotImplementedError` for the Gemini and HuggingFace providers. Please provide the correct implementation or remove them if they are not supported. 142 | * [ ] The `agent_system.py` module contains a `FIXME` note regarding the use of a try-catch context manager. Please review and implement the intended error handling. 143 | * [ ] Add TypeScript testing guidelines (if a TypeScript frontend is planned for the future). 144 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Guiding Principles 9 | 10 | - Changelogs are for humans, not machines. 11 | - There should be an entry for every single version. 12 | - The same types of changes should be grouped. 13 | - Versions and sections should be linkable. 14 | - The latest version comes first. 15 | - The release date of each version is displayed. 16 | - Mention whether you follow Semantic Versioning. 17 | 18 | ## Types of changes 19 | 20 | - `Added` for new features. 21 | - `Changed` for changes in existing functionality. 22 | - `Deprecated` for soon-to-be removed features. 23 | - `Removed` for now removed features. 24 | - `Fixed` for any bug fixes. 25 | - `Security` in case of vulnerabilities. 26 | 27 | ## [Unreleased] 28 | 29 | ## [0.0.2] - 2025-07-08 30 | 31 | ## [0.0.1] - 2025-07-07 32 | 33 | ### Added 34 | 35 | - Initial template containing templates for PRP 36 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | @AGENTS.md -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile automates the build, test, and clean processes for the project. 2 | # It provides a convenient way to run common tasks using the 'make' command. 3 | # Run `make help` to see all available recipes. 4 | 5 | .SILENT: 6 | .ONESHELL: 7 | .PHONY: all setup_python_claude setup_dev setup_prod setup_claude_code prp_gen_claude prp_exe_claude ruff test_all check_types coverage_all output_unset_app_env_sh run_example_gui run_example_server run_example_client help 8 | .DEFAULT_GOAL := help 9 | 10 | 11 | ENV_FILE := .env 12 | SRC_PATH := src 13 | APP_PATH := $(SRC_PATH) 14 | EXAMPLES_PATH := examples/mcp-server-client 15 | FEAT_DEF_PATH := /context/features 16 | PRP_DEF_PATH := /context/PRPs 17 | PRP_CLAUDE_GEN_CMD := generate-prp 18 | PRP_CLAUDE_EXE_CMD := execute-prp 19 | 20 | 21 | # MARK: setup 22 | 23 | 24 | # construct the full paths and execute Claude Code commands 25 | # TODO switch folder by function called () 26 | # TODO Claude Code non-interactive headless mode tee to CLI 27 | define CLAUDE_PRP_RUNNER 28 | echo "Starting Claude Code PRP runner ..." 29 | dest_file=$(firstword $(strip $(1))) 30 | dest_cmd=$(firstword $(strip $(2))) 31 | if [ -z "$${dest_file}" ]; then 32 | echo "Error: ARGS for PRP filename is empty. Please provide a PRP filename." 33 | exit 1 34 | fi 35 | case "$${dest_cmd}" in 36 | start) 37 | dest_cmd=$(PRP_CLAUDE_GEN_CMD) 38 | dest_path=$(FEAT_DEF_PATH);; 39 | stop) 40 | dest_cmd=$(PRP_CLAUDE_EXE_CMD) 41 | dest_path=$(PRP_DEF_PATH);; 42 | *) 43 | echo "Unknown command: $${dest_cmd}. Exiting ..." 44 | exit 1;; 45 | esac 46 | dest_cmd="/project:$${dest_cmd} $${dest_path}/$${dest_file}" 47 | echo "Executing command '$${dest_cmd}' ..." 48 | claude -p "$${dest_cmd}" 2>&1 49 | claude -p "/cost" 2>&1 50 | endef 51 | 52 | 53 | setup_python_claude: # Set up environment and install Claude Code CLI 54 | $(MAKE) -s setup_dev 55 | $(MAKE) -s export_env_file 56 | $(MAKE) -s setup_claude_code 57 | 58 | 59 | setup_dev: ## Install uv and deps, Download and start Ollama 60 | echo "Setting up dev environment ..." 61 | pip install uv -q 62 | uv sync --all-groups 63 | 64 | 65 | setup_prod: ## Install uv and deps, Download and start Ollama 66 | echo "Setting up prod environment ..." 67 | pip install uv -q 68 | uv sync --frozen 69 | 70 | 71 | setup_claude_code: ## Setup Claude Code CLI, node.js and npm have to be present 72 | echo "Setting up claude code ..." 73 | npm install -g @anthropic-ai/claude-code 74 | claude config set --global preferredNotifChannel terminal_bell 75 | echo "npm version: $$(npm --version)" 76 | claude --version 77 | 78 | 79 | export_env_file: # Read ENV_FILE and export k=v to env 80 | while IFS='=' read -r key value || [ -n "$${key}" ]; do 81 | case "$${key}" in 82 | ''|\#*) continue ;; 83 | esac 84 | value=$$(echo "$${value}" | sed -e 's/^"//' -e 's/"$$//') 85 | export "$${key}=$${value}" 86 | done < .env 87 | 88 | 89 | output_unset_env_sh: ## Unset app environment variables 90 | uf="./unset_env.sh" 91 | echo "Outputing '$${uf}' ..." 92 | printenv | awk -F= '/_API_KEY=/ {print "unset " $$1}' > $$uf 93 | 94 | 95 | # MARK: context engineering 96 | 97 | 98 | prp_gen_claude: ## generates the PRP from the file passed in "ARGS=file" 99 | $(call CLAUDE_PRP_RUNNER, $(ARGS), "generate") 100 | 101 | 102 | prp_exe_claude: ## executes the PRP from the file passed in "ARGS=file" 103 | $(call CLAUDE_PRP_RUNNER, $(ARGS), "execute") 104 | 105 | 106 | # MARK: code quality 107 | 108 | 109 | ruff: ## Lint: Format and check with ruff 110 | uv run ruff format 111 | uv run ruff check --fix 112 | 113 | 114 | test_all: ## Run all tests 115 | uv run pytest 116 | 117 | 118 | coverage_all: ## Get test coverage 119 | uv run coverage run -m pytest || true 120 | uv run coverage report -m 121 | 122 | 123 | check_types: ## Check for static typing errors 124 | uv run mypy $(APP_PATH) 125 | 126 | 127 | # MARK: run 128 | 129 | 130 | run_example_gui: ## Launch MCP server-client example GUI 131 | $(MAKE) -C $(EXAMPLES_PATH) run_gui 132 | 133 | run_example_server: ## Run MCP server-client example server 134 | $(MAKE) -C $(EXAMPLES_PATH) run_server 135 | 136 | run_example_client: ## Run MCP server-client example client 137 | $(MAKE) -C $(EXAMPLES_PATH) run_client ARGS="$(ARGS)" 138 | 139 | 140 | # MARK: help 141 | 142 | 143 | # TODO add stackoverflow source 144 | help: ## Displays this message with available recipes 145 | echo "Usage: make [recipe]" 146 | echo "Recipes:" 147 | awk '/^[a-zA-Z0-9_-]+:.*?##/ { 148 | helpMessage = match($$0, /## (.*)/) 149 | if (helpMessage) { 150 | recipe = $$1 151 | sub(/:/, "", recipe) 152 | printf " \033[36m%-20s\033[0m %s\n", recipe, substr($$0, RSTART + 3, RLENGTH) 153 | } 154 | }' $(MAKEFILE_LIST) 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Context Engineering Template 2 | 3 | This project aims to implement a template for context engineering with coding agents. As suggested by several resources, including [context-engineering-intro](https://github.com/coleam00/context-engineering-intro), [The rise of "context engineering"](https://blog.langchain.com/the-rise-of-context-engineering/), [Context Engineering](https://blog.langchain.com/context-engineering-for-agents/) and somewhat [He Built 40 Startups Using Just Prompts — Here’s His System](https://youtu.be/CIAu6WeckQ0). 4 | 5 | [![License](https://img.shields.io/badge/license-GNUGPLv3-green.svg)](LICENSE) 6 | ![Version](https://img.shields.io/badge/version-0.0.2-58f4c2) 7 | [![CodeQL](https://github.com/qte77/context-engineering-template/actions/workflows/codeql.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/codeql.yaml) 8 | [![CodeFactor](https://www.codefactor.io/repository/github/qte77/context-engineering-template/badge)](https://www.codefactor.io/repository/github/qte77/context-engineering-template) 9 | [![ruff](https://github.com/qte77/context-engineering-template/actions/workflows/ruff.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/ruff.yaml) 10 | [![pytest](https://github.com/qte77/context-engineering-template/actions/workflows/pytest.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/pytest.yaml) 11 | [![Link Checker](https://github.com/qte77/context-engineering-template/actions/workflows/links-fail-fast.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/links-fail-fast.yaml) 12 | [![Deploy Docs](https://github.com/qte77/context-engineering-template/actions/workflows/generate-deploy-mkdocs-ghpages.yaml/badge.svg)](https://github.com/qte77/context-engineering-template/actions/workflows/generate-deploy-mkdocs-ghpages.yaml) 13 | 14 | **DevEx** [![vscode.dev](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=vscode.dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://vscode.dev/github/qte77/context-engineering-template) 15 | [![Codespace Python Claude](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Codespace%20Dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://github.com/codespaces/new?repo=qte77/context-engineering-template&devcontainer_path=.devcontainer/setup_python_claude/devcontainer.json) 16 | [![TalkToGithub](https://img.shields.io/badge/TalkToGithub-7a83ff.svg)](https://talktogithub.com/qte77/context-engineering-template) 17 | [![llms.txt (UitHub)](https://img.shields.io/badge/llms.txt-uithub-800080.svg)](https://github.com/qte77/context-engineering-template) 18 | [![llms.txt (GitToDoc)](https://img.shields.io/badge/llms.txt-GitToDoc-fe4a60.svg)](https://gittodoc.com/qte77/context-engineering-template) 19 | 20 | ## Status 21 | 22 | (DRAFT) (WIP) ----> Not fully implemented yet 23 | 24 | For version history have a look at the [CHANGELOG](CHANGELOG.md). 25 | 26 | ## Purpose 27 | 28 | Let the Coding Agent do the heavy lifting. Build code base from top to bottom: Define Business Requirements (BRD) and afterwards features to be implemented. The goal could be to to implement some kind of guided top-down BDD: behavior > tests > implementation. 29 | 30 | ## Features 31 | 32 | - Runs tests, linting and type checks: only 33 | 34 |
35 | Show Sequence Diagram 36 | 37 | Sequence Diagram 38 | 39 | 40 | Sequence Diagram 41 | 42 |
43 | 44 | ## Setup 45 | 46 | 1. `make setup_python_claude` 47 | 2. If .env to be used: `make export_env_file` 48 | 49 | ## Usage 50 | 51 | 1. Update [Agents.md](AGENTS.md) to your needs. 52 | 2. Describe desired feature in `/context/features/feature_XXX.md`, like shown in [feature_base.md](/context/templates/feature_base.md). 53 | 3. Place optional examples into [/context/examples](/context/examples). 54 | 4. Let the Product Requirements Prompt (PRP) be generated: 55 | - In Claude Code CLI: `/generate-prp feature_XXX.md` 56 | - or: `make prp_gen_claude "ARGS=feature_XXX.md"` 57 | 5. Let the feature be implemented based on the PRP: 58 | - In Claude Code CLI: `/execute-prp feature_XXX.md` 59 | - or: `make prp_exe_claude "ARGS=feature_XXX.md"` 60 | 61 | ### Configuration 62 | 63 | - General system behavior: `AGENTS.md`, redirected from `CLAUDE.md` 64 | - Claude settings: `.claude/settings.local.json` 65 | - CLaude commands: `.claude/commands` 66 | - Feature template: `context/templates/feature_base.md` 67 | - PRP template: `context/templates/prp_base.md` 68 | 69 | ### Environment 70 | 71 | [.env.example](.env.example) contains examples for usage of API keys and variables. 72 | 73 | ```text 74 | ANTHROPIC_API_KEY="sk-abc-xyz" 75 | GEMINI_API_KEY="xyz" 76 | GITHUB_API_KEY="ghp_xyz" 77 | ... 78 | ``` 79 | 80 | ## TODO 81 | 82 | - Implement business process as discussed in [He Built 40 Startups Using Just Prompts — Here’s His System](https://youtu.be/CIAu6WeckQ0) 83 | - Refine `AGENTS.md` to let the agent not do bulk but incremental changes, also implement tests first, then code and iterate until functional (red > green > blue). 84 | -------------------------------------------------------------------------------- /assets/images/example_execute_feature_mcp_server_PRP_update_pyproject.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/assets/images/example_execute_feature_mcp_server_PRP_update_pyproject.PNG -------------------------------------------------------------------------------- /assets/images/example_execute_feature_mcp_server_PRP_update_pyproject_diff.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/assets/images/example_execute_feature_mcp_server_PRP_update_pyproject_diff.PNG -------------------------------------------------------------------------------- /assets/images/example_execute_feature_mcp_server_created.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/assets/images/example_execute_feature_mcp_server_created.PNG -------------------------------------------------------------------------------- /assets/images/example_generate_feature_mcp_server_PRP.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/assets/images/example_generate_feature_mcp_server_PRP.PNG -------------------------------------------------------------------------------- /assets/images/sequence_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/assets/images/sequence_diagram.png -------------------------------------------------------------------------------- /context/templates/feature_base.md: -------------------------------------------------------------------------------- 1 | # Feature description for: [ Initial template for new features ] 2 | 3 | ## FEATURE 4 | 5 | [Insert your feature here] 6 | 7 | ## EXAMPLES 8 | 9 | [Provide and explain examples that you have in the `/context/examples` folder] 10 | 11 | ## DOCUMENTATION 12 | 13 | [List out any documentation (web pages, sources for an MCP server like Crawl4AI RAG, etc.) that will need to be referenced during development] 14 | 15 | ## OTHER CONSIDERATIONS 16 | 17 | [Any other considerations or specific requirements - great place to include gotchas that you see AI coding assistants miss with your projects a lot] 18 | -------------------------------------------------------------------------------- /context/templates/prp_base.md: -------------------------------------------------------------------------------- 1 | # "Base PRP Template v2 - Context-Rich with Validation Loops" 2 | 3 | ## Purpose 4 | 5 | Product Requirements Prompt (PRP) Template optimized for AI agents to implement features with sufficient context and self-validation capabilities to achieve working code through iterative refinement. 6 | 7 | ## Core Principles 8 | 9 | 1. **Context is King**: Include ALL necessary documentation, examples, and caveats 10 | 2. **Validation Loops**: Provide executable tests/lints the AI can run and fix 11 | 3. **Information Dense**: Use keywords and patterns from the codebase 12 | 4. **Progressive Success**: Start simple, validate, then enhance 13 | 5. **Global rules**: Be sure to follow all rules in CLAUDE.md 14 | 15 | --- 16 | 17 | ## Goal 18 | 19 | [What needs to be built - be specific about the end state and desires] 20 | 21 | ## Why 22 | 23 | - [Business value and user impact] 24 | - [Integration with existing features] 25 | - [Problems this solves and for whom] 26 | 27 | ## What 28 | 29 | [User-visible behavior and technical requirements] 30 | 31 | ### Success Criteria 32 | 33 | - [ ] [Specific measurable outcomes] 34 | 35 | ## All Needed Context 36 | 37 | ### Documentation & References (list all context needed to implement the feature) 38 | 39 | ```yaml 40 | # MUST READ - Include these in your context window 41 | - url: [Official API docs URL] 42 | why: [Specific sections/methods you'll need] 43 | 44 | - file: [path/to/example.py] 45 | why: [Pattern to follow, gotchas to avoid] 46 | 47 | - doc: [Library documentation URL] 48 | section: [Specific section about common pitfalls] 49 | critical: [Key insight that prevents common errors] 50 | 51 | - docfile: [PRPs/ai_docs/file.md] 52 | why: [docs that the user has pasted in to the project] 53 | ``` 54 | 55 | ### Current Codebase tree (run `tree` in the root of the project) to get an overview of the codebase 56 | 57 | ```bash 58 | 59 | ``` 60 | 61 | ### Desired Codebase tree with files to be added and responsibility of file 62 | 63 | ```bash 64 | 65 | ``` 66 | 67 | ### Known Gotchas of our codebase & Library Quirks 68 | 69 | ```python 70 | # CRITICAL: [Library name] requires [specific setup] 71 | # Example: FastAPI requires async functions for endpoints 72 | # Example: This ORM doesn't support batch inserts over 1000 records 73 | # Example: We use pydantic v2 and 74 | ``` 75 | 76 | ## Implementation Blueprint 77 | 78 | ### Data models and structure 79 | 80 | Create the core data models, we ensure type safety and consistency. 81 | 82 | ```python 83 | Examples: 84 | - orm models 85 | - pydantic models 86 | - pydantic schemas 87 | - pydantic validators 88 | ``` 89 | 90 | ### list of tasks to be completed to fullfill the PRP in the order they should be completed 91 | 92 | ```yaml 93 | Task 1: 94 | MODIFY src/existing_module.py: 95 | - FIND pattern: "class OldImplementation" 96 | - INJECT after line containing "def __init__" 97 | - PRESERVE existing method signatures 98 | 99 | CREATE src/new_feature.py: 100 | - MIRROR pattern from: src/similar_feature.py 101 | - MODIFY class name and core logic 102 | - KEEP error handling pattern identical 103 | 104 | ...(...) 105 | 106 | Task N: 107 | ... 108 | 109 | ``` 110 | 111 | ### Per task pseudocode as needed added to each task 112 | 113 | ```python 114 | 115 | # Task 1 116 | # Pseudocode with CRITICAL details dont write entire code 117 | async def new_feature(param: str) -> Result: 118 | # PATTERN: Always validate input first (see src/validators.py) 119 | validated = validate_input(param) # raises ValidationError 120 | 121 | # GOTCHA: This library requires connection pooling 122 | async with get_connection() as conn: # see src/db/pool.py 123 | # PATTERN: Use existing retry decorator 124 | @retry(attempts=3, backoff=exponential) 125 | async def _inner(): 126 | # CRITICAL: API returns 429 if >10 req/sec 127 | await rate_limiter.acquire() 128 | return await external_api.call(validated) 129 | 130 | result = await _inner() 131 | 132 | # PATTERN: Standardized response format 133 | return format_response(result) # see src/utils/responses.py 134 | ``` 135 | 136 | ### Integration Points 137 | 138 | ```yaml 139 | DATABASE: 140 | - migration: "Add column 'feature_enabled' to users table" 141 | - index: "CREATE INDEX idx_feature_lookup ON users(feature_id)" 142 | 143 | CONFIG: 144 | - add to: config/settings.py 145 | - pattern: "FEATURE_TIMEOUT = int(os.getenv('FEATURE_TIMEOUT', '30'))" 146 | 147 | ROUTES: 148 | - add to: src/api/routes.py 149 | - pattern: "router.include_router(feature_router, prefix='/feature')" 150 | ``` 151 | 152 | ## Validation Loop 153 | 154 | ### Level 1: Syntax & Style 155 | 156 | ```bash 157 | # Run these FIRST - fix any errors before proceeding 158 | ruff check src/new_feature.py --fix # Auto-fix what's possible 159 | mypy src/new_feature.py # Type checking 160 | 161 | # Expected: No errors. If errors, READ the error and fix. 162 | ``` 163 | 164 | ### Level 2: Unit Tests each new feature/file/function use existing test patterns 165 | 166 | ```python 167 | # CREATE test_new_feature.py with these test cases: 168 | def test_happy_path(): 169 | """Basic functionality works""" 170 | result = new_feature("valid_input") 171 | assert result.status == "success" 172 | 173 | def test_validation_error(): 174 | """Invalid input raises ValidationError""" 175 | with pytest.raises(ValidationError): 176 | new_feature("") 177 | 178 | def test_external_api_timeout(): 179 | """Handles timeouts gracefully""" 180 | with mock.patch('external_api.call', side_effect=TimeoutError): 181 | result = new_feature("valid") 182 | assert result.status == "error" 183 | assert "timeout" in result.message 184 | ``` 185 | 186 | ```bash 187 | # Run and iterate until passing: 188 | uv run pytest test_new_feature.py -v 189 | # If failing: Read error, understand root cause, fix code, re-run (never mock to pass) 190 | ``` 191 | 192 | ### Level 3: Integration Test 193 | 194 | ```bash 195 | # Start the service 196 | uv run python -m src.main --dev 197 | 198 | # Test the endpoint 199 | curl -X POST http://localhost:8000/feature \ 200 | -H "Content-Type: application/json" \ 201 | -d '{"param": "test_value"}' 202 | 203 | # Expected: {"status": "success", "data": {...}} 204 | # If error: Check logs at logs/app.log for stack trace 205 | ``` 206 | 207 | ## Final validation Checklist 208 | 209 | - [ ] All tests pass: `uv run pytest tests/ -v` 210 | - [ ] No linting errors: `uv run ruff check src/` 211 | - [ ] No type errors: `uv run mypy src/` 212 | - [ ] Manual test successful: [specific curl/command] 213 | - [ ] Error cases handled gracefully 214 | - [ ] Logs are informative but not verbose 215 | - [ ] Documentation updated if needed 216 | 217 | --- 218 | 219 | ## Anti-Patterns to Avoid 220 | 221 | - ❌ Don't create new patterns when existing ones work 222 | - ❌ Don't skip validation because "it should work" 223 | - ❌ Don't ignore failing tests - fix them 224 | - ❌ Don't use sync functions in async context 225 | - ❌ Don't hardcode values that should be config 226 | - ❌ Don't catch all exceptions - be specific 227 | -------------------------------------------------------------------------------- /docs/architecture/sequence_diagram.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant User 3 | participant feature_DESC_XXX.md 4 | participant ClaudeCode 5 | participant generate-prp.md 6 | participant feature_PRP_XXX.md 7 | participant execute-prp.md 8 | participant AI Coding Agent 9 | 10 | User->>/context/features/feature_DESC_XXX.md: Describes feature 11 | User->>ClaudeCode: Runs /generate-prp.md /context/features/feature_DESC_XXX.md 12 | ClaudeCode->>generate-prp.md: Executes command 13 | generate-prp.md->>AI Coding Agent: Reads /context/features/feature_DESC_XXX.md, codebase, and web 14 | AI Coding Agent-->>generate-prp.md: Researches codebase, docs, and examples 15 | generate-prp.md->>/context/features/feature_PRP_XXX.md: Generates PRP 16 | ClaudeCode->>User: PRP saved in /context/PRPs/feature_PRP_XXX.md 17 | User->>ClaudeCode: Runs /execute-prp /context/PRPs/feature_PRP_XXX.md 18 | ClaudeCode->>execute-prp.md: Executes command 19 | execute-prp.md->>AI Coding Agent: Reads /context/PRPs/feature_PRP_XXX.md and CLAUDE.md 20 | AI Coding Agent->>AI Coding Agent: Creates implementation plan, executes, validates, and iterates 21 | AI Coding Agent-->>User: Implements feature 22 | -------------------------------------------------------------------------------- /docs/llms.txt: -------------------------------------------------------------------------------- 1 | 404 - Failed to fetch repository. If the repo is private, be sure to provide an Authorization header. Status:404 -------------------------------------------------------------------------------- /examples/mcp-server-client/Makefile: -------------------------------------------------------------------------------- 1 | # MCP Server-Client Example Makefile 2 | # This Makefile provides commands for the MCP server-client example 3 | 4 | .SILENT: 5 | .ONESHELL: 6 | .PHONY: setup_dev setup_prod ruff test_all check_types coverage_all run_gui run_server run_client help 7 | .DEFAULT_GOAL := help 8 | 9 | SRC_PATH := src 10 | APP_PATH := $(SRC_PATH) 11 | 12 | # MARK: setup 13 | 14 | setup_dev: ## Install uv and development dependencies 15 | echo "Setting up dev environment ..." 16 | pip install uv -q 17 | uv sync --all-groups 18 | 19 | setup_prod: ## Install uv and production dependencies 20 | echo "Setting up prod environment ..." 21 | pip install uv -q 22 | uv sync --frozen 23 | 24 | # MARK: code quality 25 | 26 | ruff: ## Format and lint code with ruff 27 | uv run ruff format 28 | uv run ruff check --fix 29 | 30 | test_all: ## Run all tests 31 | uv run pytest 32 | 33 | coverage_all: ## Get test coverage 34 | uv run coverage run -m pytest || true 35 | uv run coverage report -m 36 | 37 | check_types: ## Check for static typing errors 38 | uv run mypy $(APP_PATH) 39 | 40 | # MARK: run 41 | 42 | run_gui: ## Launch Streamlit GUI 43 | uv run python -m src.main gui 44 | 45 | run_server: ## Run MCP server 46 | uv run python -m src.main server 47 | 48 | run_client: ## Run MCP client (requires ARGS) 49 | uv run python -m src.main client $(ARGS) 50 | 51 | # MARK: help 52 | 53 | help: ## Display available commands 54 | echo "Usage: make [command]" 55 | echo "Commands:" 56 | awk '/^[a-zA-Z0-9_-]+:.*?##/ { 57 | helpMessage = match($$0, /## (.*)/) 58 | if (helpMessage) { 59 | recipe = $$1 60 | sub(/:/, "", recipe) 61 | printf " \033[36m%-20s\033[0m %s\n", recipe, substr($$0, RSTART + 3, RLENGTH) 62 | } 63 | }' $(MAKEFILE_LIST) -------------------------------------------------------------------------------- /examples/mcp-server-client/context/features/feature_1_mcp_server.md: -------------------------------------------------------------------------------- 1 | # Feature description for: MCP Server with tools 2 | 3 | ## FEATURE 4 | 5 | Implement an **MCP (Message Control Protocol) Server** in Python that exposes three callable tools via structured messages. The server should receive well-formed MCP messages and dispatch tool invocations accordingly. The three tools to be exposed are: 6 | 7 | 1. **Roll Dice**: Accepts a format like `2d6` or `1d20` and returns the rolled values and total. 8 | 2. **Get Weather**: Accepts a city name or coordinates and returns the current weather conditions using a public weather API. 9 | 3. **Get Date**: Returns the current date and time in ISO 8601 format or based on a requested timezone. 10 | 11 | The server should be modular, testable, and extensible for future tools. Logging, error handling, and message validation should be considered first-class concerns. 12 | 13 | ## EXAMPLES 14 | 15 | Located in `/context/examples`: 16 | 17 | * `roll_dice_example.json`: Demonstrates sending `{"tool": "roll_dice", "args": {"notation": "3d6"}}` and receiving `{"result": {"values": [4,2,6], "total": 12}}`. 18 | * `get_weather_example.json`: Sends `{"tool": "get_weather", "args": {"location": "San Francisco"}}` and expects weather data such as temperature, condition, and wind speed. 19 | * `get_date_example.json`: Sends `{"tool": "get_date", "args": {"timezone": "UTC"}}` and receives `{"result": "2025-07-06T16:22:00Z"}`. 20 | 21 | These examples cover correct usage and malformed inputs to validate tool response and error handling. 22 | 23 | ## DOCUMENTATION 24 | 25 | * [Open-Meteo API](https://open-meteo.com/en/docs): For retrieving weather information. 26 | * [Python `datetime` module](https://docs.python.org/3/library/datetime.html): For implementing date and time tool. 27 | * [random module (Python)](https://docs.python.org/3/library/random.html): For rolling dice. 28 | * \[MCP Protocol Overview (proprietary/internal if applicable)] or general protocol documentation, if using a specific spec. 29 | 30 | Additional context from [context-engineering-intro](https://github.com/qte77/context-engineering-template) will inform message structure and processing strategy. 31 | 32 | ## OTHER CONSIDERATIONS 33 | 34 | * **Tool routing logic** should be clearly separated to allow clean expansion. 35 | * **Input validation** is critical: especially for `roll_dice`, invalid formats (e.g., `3x5`, `0d6`, `d10`) must return informative errors. 36 | * **Weather API failures or rate limits** should be gracefully handled with fallback messages. 37 | * **Timezone parsing** for `get_date` should use `pytz` or `zoneinfo`, and clearly inform users when timezones are unsupported. 38 | * **Security note**: Weather and date APIs should not expose sensitive request metadata or leak internal server details in errors. 39 | * AI coding assistants often: 40 | 41 | * Miss edge case handling (e.g., zero dice, negative sides) 42 | * Forget to structure results consistently across tools 43 | * Fail to modularize tool logic, making future expansion difficult 44 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/features/feature_2_mcp_client.md: -------------------------------------------------------------------------------- 1 | # Feature description for: MCP Client for Tool Invocation 2 | 3 | ## FEATURE 4 | 5 | Implement a **Python-based MCP Client** capable of sending structured requests to an MCP Server and handling the corresponding responses. The client should: 6 | 7 | * Connect to the MCP server over a socket, HTTP, or another configured protocol. 8 | * Serialize requests into the expected MCP message format (e.g., JSON or line-based protocol). 9 | * Provide a command-line interface (CLI) and/or programmatic interface for interacting with the following tools: 10 | 11 | 1. **Roll Dice** (`roll_dice`) – accepts dice notation like `2d6`, `1d20`. 12 | 2. **Get Weather** (`get_weather`) – accepts a location name or coordinates. 13 | 3. **Get Date** (`get_date`) – optionally accepts a timezone. 14 | 15 | The client should also handle connection errors, invalid tool responses, and retry logic gracefully. 16 | 17 | ## EXAMPLES 18 | 19 | Located in `/context/examples`: 20 | 21 | * `client_roll_dice_input.json`: `{ "tool": "roll_dice", "args": { "notation": "2d6" } }` 22 | * `client_get_weather_input.json`: `{ "tool": "get_weather", "args": { "location": "Berlin" } }` 23 | * `client_get_date_input.json`: `{ "tool": "get_date", "args": { "timezone": "UTC" } }` 24 | * `client_invalid_tool.json`: `{ "tool": "fly_to_mars", "args": {} }` → Should trigger a meaningful error from the server 25 | 26 | These example requests and expected responses can be used for local testing and automated integration checks. 27 | 28 | ## DOCUMENTATION 29 | 30 | * [Python `socket` module](https://docs.python.org/3/library/socket.html) or [requests](https://docs.python.org/3/library/urllib.request.html) depending on transport. 31 | * [JSON module](https://docs.python.org/3/library/json.html) for message formatting. 32 | * [argparse](https://docs.python.org/3/library/argparse.html) for implementing a simple CLI wrapper. 33 | * Reference the MCP Server protocol spec or internal documentation (e.g. *MCP Protocol Overview* if proprietary). 34 | * [context-engineering-template](https://github.com/qte/context-engineering-template) for usage conventions. 35 | 36 | ## OTHER CONSIDERATIONS 37 | 38 | * Client must validate outgoing messages before sending to avoid malformed requests. 39 | * Handle connection errors, timeouts, and retries in a user-friendly manner. 40 | * The response handler should check for required fields (`result`, `error`, etc.) to avoid crashes on malformed server responses. 41 | * Consider pluggability of tools so future expansions can be supported with minimal refactoring. 42 | * AI assistants often: 43 | 44 | * Miss error handling around partial or no server responses. 45 | * Forget to properly close socket connections or handle timeouts. 46 | * Write overly rigid request builders, making CLI usage frustrating. 47 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/features/feature_3_streamlit_gui.md: -------------------------------------------------------------------------------- 1 | # Feature description for: Streamlit GUI for MCP Server-Client Interaction Showcase 2 | 3 | ## FEATURE 4 | 5 | Develop a **Streamlit-based graphical user interface (GUI)** to demonstrate and interactively showcase the communication and integration between the MCP Server and MCP Client. The GUI should allow users to: 6 | 7 | * Select and invoke any of the three available tools (`roll_dice`, `get_weather`, `get_date`) via intuitive form inputs. 8 | * Enter tool-specific parameters such as dice notation, location, or timezone. 9 | * Display real-time request payloads sent by the client and the corresponding responses received from the server. 10 | * Handle and display error messages gracefully. 11 | * Log interaction history for the current session, allowing users to review previous commands and results. 12 | * Provide clear visual feedback about the status of the connection and request execution. 13 | 14 | This GUI acts as both a testing ground and demonstration interface, useful for users unfamiliar with command-line tools or raw protocol messages. 15 | 16 | ## EXAMPLES 17 | 18 | Located in `/context/examples`: 19 | 20 | * `streamlit_roll_dice_interaction.json`: Example input/output pairs demonstrating a dice roll session in the GUI. 21 | * `streamlit_get_weather_interaction.json`: Demonstrates user inputs for location and the displayed weather response. 22 | * `streamlit_get_date_interaction.json`: Shows date/time requests with optional timezone selection. 23 | * `streamlit_error_handling.json`: Examples of how the GUI displays server-side validation errors or connection issues. 24 | 25 | These examples serve as test cases for GUI input validation and response rendering. 26 | 27 | ## DOCUMENTATION 28 | 29 | * [Streamlit Documentation](https://docs.streamlit.io/) for building interactive Python apps. 30 | * \[MCP Server and Client Protocol Specs] (internal/proprietary or from context-engineering-intro). 31 | * Python libraries for HTTP or socket communication used by the client. 32 | * UI/UX design best practices for interactive demos. 33 | * [context-engineering-intro](https://github.com/coleam00/context-engineering-intro) for project conventions. 34 | 35 | ## OTHER CONSIDERATIONS 36 | 37 | * Ensure asynchronous or non-blocking communication so the UI remains responsive during server interactions. 38 | * Validate inputs in the GUI before sending to the client to minimize server errors. 39 | * Provide helpful tooltips or inline help to explain tool parameters to users unfamiliar with dice notation or timezone formats. 40 | * Consider session state management in Streamlit to maintain history and status. 41 | * AI coding assistants often overlook proper error propagation to the UI and user-friendly messaging. 42 | * Security considerations: if exposing any sensitive endpoints or API keys, avoid hardcoding secrets in the GUI code. 43 | * Design with extensibility in mind to add new tools or more complex workflows easily. 44 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/client_get_date_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Simple client input format for get_date tool", 3 | "cli_command": "python -m src.main client --server src/mcp_server/server.py get_date --timezone UTC", 4 | "expected_input": { 5 | "tool": "get_date", 6 | "arguments": { 7 | "timezone": "UTC" 8 | } 9 | }, 10 | "expected_output_format": { 11 | "success": true, 12 | "tool_name": "get_date", 13 | "result": { 14 | "content": [ 15 | { 16 | "type": "text", 17 | "text": "🕐 **Current Date & Time**\n📅 Date: **2025-07-07** (Monday)\n⏰ Time: **14:30:25**\n🌍 Timezone: **UTC**\n📋 ISO 8601: `2025-07-07T14:30:25+00:00`\n🔢 Unix Timestamp: `1720360225`" 18 | } 19 | ] 20 | } 21 | }, 22 | "examples": [ 23 | { 24 | "timezone": "America/New_York", 25 | "description": "Get Eastern Time" 26 | }, 27 | { 28 | "timezone": "Europe/London", 29 | "description": "Get London time" 30 | }, 31 | { 32 | "timezone": "Asia/Tokyo", 33 | "description": "Get Tokyo time" 34 | }, 35 | { 36 | "timezone": "pst", 37 | "description": "Get Pacific Time using alias" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/client_get_weather_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Simple client input format for get_weather tool", 3 | "cli_command": "python -m src.main client --server src/mcp_server/server.py get_weather --location 'San Francisco'", 4 | "expected_input": { 5 | "tool": "get_weather", 6 | "arguments": { 7 | "location": "San Francisco" 8 | } 9 | }, 10 | "expected_output_format": { 11 | "success": true, 12 | "tool_name": "get_weather", 13 | "result": { 14 | "content": [ 15 | { 16 | "type": "text", 17 | "text": "🌤️ **Weather for San Francisco**\n🌡️ Temperature: **18.5°C**\n☁️ Condition: **Partly cloudy**\n💨 Wind Speed: **12.3 km/h**\n💧 Humidity: **65%**\n🕐 Updated: 2025-07-07 14:30 UTC" 18 | } 19 | ] 20 | } 21 | }, 22 | "examples": [ 23 | { 24 | "location": "London", 25 | "description": "Get weather for London" 26 | }, 27 | { 28 | "location": "New York", 29 | "description": "Get weather for New York" 30 | }, 31 | { 32 | "location": "37.7749,-122.4194", 33 | "description": "Get weather using coordinates" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/client_invalid_tool.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Error handling example for invalid tool", 3 | "cli_command": "python -m src.main client --server src/mcp_server/server.py invalid_tool --arg value", 4 | "expected_input": { 5 | "tool": "invalid_tool", 6 | "arguments": { 7 | "arg": "value" 8 | } 9 | }, 10 | "expected_output_format": { 11 | "success": false, 12 | "tool_name": "invalid_tool", 13 | "error": "Tool 'invalid_tool' not available. Available tools: ['roll_dice', 'get_weather', 'get_date']", 14 | "arguments": { 15 | "arg": "value" 16 | } 17 | }, 18 | "error_scenarios": [ 19 | { 20 | "scenario": "Tool not available", 21 | "tool": "nonexistent_tool", 22 | "expected_error": "Tool 'nonexistent_tool' not available" 23 | }, 24 | { 25 | "scenario": "Server not running", 26 | "server_path": "./nonexistent_server.py", 27 | "expected_error": "Server script not found" 28 | }, 29 | { 30 | "scenario": "Invalid server path", 31 | "server_path": "/dev/null", 32 | "expected_error": "Failed to connect to server" 33 | }, 34 | { 35 | "scenario": "Connection timeout", 36 | "timeout": 1, 37 | "expected_error": "Connection timeout" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/client_roll_dice_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Simple client input format for roll_dice tool", 3 | "cli_command": "python -m src.main client --server src/mcp_server/server.py roll_dice --notation 2d6", 4 | "expected_input": { 5 | "tool": "roll_dice", 6 | "arguments": { 7 | "notation": "2d6" 8 | } 9 | }, 10 | "expected_output_format": { 11 | "success": true, 12 | "tool_name": "roll_dice", 13 | "result": { 14 | "content": [ 15 | { 16 | "type": "text", 17 | "text": "🎲 Rolled 2d6: [3, 5] = **8**" 18 | } 19 | ] 20 | } 21 | }, 22 | "examples": [ 23 | { 24 | "notation": "1d20", 25 | "description": "Roll a 20-sided die" 26 | }, 27 | { 28 | "notation": "3d6", 29 | "description": "Roll three 6-sided dice" 30 | }, 31 | { 32 | "notation": "2d10", 33 | "description": "Roll two 10-sided dice" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/get_date_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Example request and response for the get_date tool", 3 | "request": { 4 | "jsonrpc": "2.0", 5 | "method": "tools/call", 6 | "params": { 7 | "name": "get_date", 8 | "arguments": { 9 | "timezone": "UTC" 10 | } 11 | }, 12 | "id": 3 13 | }, 14 | "response": { 15 | "jsonrpc": "2.0", 16 | "id": 3, 17 | "result": { 18 | "content": [ 19 | { 20 | "type": "text", 21 | "text": "🕐 **Current Date & Time**\n📅 Date: **2025-07-07** (Monday)\n⏰ Time: **14:30:25**\n🌍 Timezone: **UTC**\n📋 ISO 8601: `2025-07-07T14:30:25+00:00`\n🔢 Unix Timestamp: `1720360225`" 22 | } 23 | ], 24 | "isError": false 25 | } 26 | }, 27 | "examples": { 28 | "valid_requests": [ 29 | { 30 | "timezone": "UTC", 31 | "description": "Coordinated Universal Time" 32 | }, 33 | { 34 | "timezone": "America/New_York", 35 | "description": "Eastern Time" 36 | }, 37 | { 38 | "timezone": "America/Los_Angeles", 39 | "description": "Pacific Time" 40 | }, 41 | { 42 | "timezone": "Europe/London", 43 | "description": "British Time" 44 | }, 45 | { 46 | "timezone": "Asia/Tokyo", 47 | "description": "Japan Standard Time" 48 | }, 49 | { 50 | "timezone": "est", 51 | "description": "Eastern Time alias" 52 | }, 53 | { 54 | "timezone": "pst", 55 | "description": "Pacific Time alias" 56 | } 57 | ], 58 | "invalid_requests": [ 59 | { 60 | "timezone": "Invalid/Timezone", 61 | "error": "Invalid timezone: 'Invalid/Timezone'. Common timezones: UTC, America/New_York, America/Los_Angeles, America/Chicago, Europe/London, Europe/Paris, Asia/Tokyo, Australia/Sydney. Aliases: utc, gmt, est, pst, cst, mst, edt, pdt, cdt, mdt, bst, cet, jst, aest. Use IANA timezone names (e.g., 'America/New_York') or aliases." 62 | }, 63 | { 64 | "timezone": "", 65 | "error": "Timezone cannot be empty" 66 | } 67 | ] 68 | }, 69 | "supported_timezones": { 70 | "aliases": { 71 | "utc": "UTC", 72 | "gmt": "UTC", 73 | "est": "America/New_York", 74 | "pst": "America/Los_Angeles", 75 | "cst": "America/Chicago", 76 | "mst": "America/Denver", 77 | "edt": "America/New_York", 78 | "pdt": "America/Los_Angeles", 79 | "cdt": "America/Chicago", 80 | "mdt": "America/Denver", 81 | "bst": "Europe/London", 82 | "cet": "Europe/Paris", 83 | "jst": "Asia/Tokyo", 84 | "aest": "Australia/Sydney" 85 | }, 86 | "common_iana_zones": [ 87 | "UTC", 88 | "America/New_York", 89 | "America/Los_Angeles", 90 | "America/Chicago", 91 | "America/Denver", 92 | "Europe/London", 93 | "Europe/Paris", 94 | "Europe/Berlin", 95 | "Asia/Tokyo", 96 | "Asia/Shanghai", 97 | "Australia/Sydney" 98 | ] 99 | } 100 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/get_weather_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Example request and response for the get_weather tool", 3 | "request": { 4 | "jsonrpc": "2.0", 5 | "method": "tools/call", 6 | "params": { 7 | "name": "get_weather", 8 | "arguments": { 9 | "location": "San Francisco" 10 | } 11 | }, 12 | "id": 2 13 | }, 14 | "response": { 15 | "jsonrpc": "2.0", 16 | "id": 2, 17 | "result": { 18 | "content": [ 19 | { 20 | "type": "text", 21 | "text": "🌤️ **Weather for San Francisco**\n🌡️ Temperature: **18.5°C**\n☁️ Condition: **Partly cloudy**\n💨 Wind Speed: **12.3 km/h**\n💧 Humidity: **65%**\n🕐 Updated: 2025-07-07 14:30 UTC" 22 | } 23 | ], 24 | "isError": false 25 | } 26 | }, 27 | "examples": { 28 | "valid_requests": [ 29 | { 30 | "location": "London", 31 | "description": "Weather for London city" 32 | }, 33 | { 34 | "location": "New York", 35 | "description": "Weather for New York city" 36 | }, 37 | { 38 | "location": "37.7749,-122.4194", 39 | "description": "Weather using coordinates (San Francisco)" 40 | }, 41 | { 42 | "location": "Tokyo", 43 | "description": "Weather for Tokyo city" 44 | } 45 | ], 46 | "invalid_requests": [ 47 | { 48 | "location": "Unknown City", 49 | "error": "Unknown location: 'Unknown City'. Please use coordinates (lat,lon) or one of: berlin, beijing, cairo, chicago, lagos, london, los angeles, madrid, miami, moscow, mumbai, new york, paris, rome, san francisco, seattle, sydney, tokyo, toronto, vancouver" 50 | }, 51 | { 52 | "location": "", 53 | "error": "Location cannot be empty" 54 | }, 55 | { 56 | "location": "999,999", 57 | "error": "Unknown location: '999,999'. Please use coordinates (lat,lon) or one of: [city list]" 58 | } 59 | ] 60 | }, 61 | "supported_cities": [ 62 | "San Francisco", 63 | "New York", 64 | "London", 65 | "Paris", 66 | "Tokyo", 67 | "Sydney", 68 | "Los Angeles", 69 | "Chicago", 70 | "Miami", 71 | "Seattle", 72 | "Vancouver", 73 | "Toronto", 74 | "Berlin", 75 | "Rome", 76 | "Madrid", 77 | "Moscow", 78 | "Beijing", 79 | "Mumbai", 80 | "Cairo", 81 | "Lagos" 82 | ] 83 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/roll_dice_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Example request and response for the roll_dice tool", 3 | "request": { 4 | "jsonrpc": "2.0", 5 | "method": "tools/call", 6 | "params": { 7 | "name": "roll_dice", 8 | "arguments": { 9 | "notation": "3d6" 10 | } 11 | }, 12 | "id": 1 13 | }, 14 | "response": { 15 | "jsonrpc": "2.0", 16 | "id": 1, 17 | "result": { 18 | "content": [ 19 | { 20 | "type": "text", 21 | "text": "🎲 Rolled 3d6: [4, 2, 6] = **12**" 22 | } 23 | ], 24 | "isError": false 25 | } 26 | }, 27 | "examples": { 28 | "valid_requests": [ 29 | { 30 | "notation": "1d20", 31 | "description": "Single twenty-sided die" 32 | }, 33 | { 34 | "notation": "2d6", 35 | "description": "Two six-sided dice" 36 | }, 37 | { 38 | "notation": "4d10", 39 | "description": "Four ten-sided dice" 40 | } 41 | ], 42 | "invalid_requests": [ 43 | { 44 | "notation": "d6", 45 | "error": "Invalid dice notation: 'd6'. Expected format: 'XdY' (e.g., '2d6', '1d20')" 46 | }, 47 | { 48 | "notation": "0d6", 49 | "error": "Dice count must be greater than 0" 50 | }, 51 | { 52 | "notation": "101d6", 53 | "error": "Dice count must not exceed 100" 54 | }, 55 | { 56 | "notation": "1d0", 57 | "error": "Number of sides must be greater than 0" 58 | }, 59 | { 60 | "notation": "abc", 61 | "error": "Invalid dice notation: 'abc'. Expected format: 'XdY' (e.g., '2d6', '1d20')" 62 | } 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/streamlit_error_handling.json: -------------------------------------------------------------------------------- 1 | { 2 | "interaction_type": "streamlit_gui", 3 | "tool_name": "roll_dice", 4 | "timestamp": "2024-01-15T10:36:00Z", 5 | "request": { 6 | "method": "tool_invocation", 7 | "parameters": { 8 | "tool": "roll_dice", 9 | "arguments": { 10 | "notation": "invalid_dice" 11 | } 12 | } 13 | }, 14 | "response": { 15 | "success": false, 16 | "result": null, 17 | "error": "Invalid dice notation format: invalid_dice", 18 | "tool_name": "roll_dice", 19 | "arguments": { 20 | "notation": "invalid_dice" 21 | } 22 | }, 23 | "execution_time": 0.05, 24 | "gui_state": { 25 | "connected": true, 26 | "server_path": "src/mcp_server/server.py", 27 | "available_tools": ["roll_dice", "get_weather", "get_date"] 28 | }, 29 | "error_details": { 30 | "validation_error": "Dice notation must follow pattern: NdN (e.g., 2d6, 1d20)", 31 | "user_input": "invalid_dice", 32 | "suggested_corrections": ["2d6", "1d20", "3d10"] 33 | } 34 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/streamlit_get_date_interaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "interaction_type": "streamlit_gui", 3 | "tool_name": "get_date", 4 | "timestamp": "2024-01-15T10:34:00Z", 5 | "request": { 6 | "method": "tool_invocation", 7 | "parameters": { 8 | "tool": "get_date", 9 | "arguments": { 10 | "timezone": "America/New_York" 11 | } 12 | } 13 | }, 14 | "response": { 15 | "success": true, 16 | "result": { 17 | "datetime": "2024-01-15T05:34:15-05:00", 18 | "timezone": "America/New_York", 19 | "formatted": "Monday, January 15, 2024 at 5:34:15 AM EST", 20 | "unix_timestamp": 1705313655 21 | }, 22 | "tool_name": "get_date", 23 | "arguments": { 24 | "timezone": "America/New_York" 25 | } 26 | }, 27 | "execution_time": 0.12, 28 | "gui_state": { 29 | "connected": true, 30 | "server_path": "src/mcp_server/server.py", 31 | "available_tools": ["roll_dice", "get_weather", "get_date"] 32 | } 33 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/streamlit_get_weather_interaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "interaction_type": "streamlit_gui", 3 | "tool_name": "get_weather", 4 | "timestamp": "2024-01-15T10:32:00Z", 5 | "request": { 6 | "method": "tool_invocation", 7 | "parameters": { 8 | "tool": "get_weather", 9 | "arguments": { 10 | "location": "San Francisco" 11 | } 12 | } 13 | }, 14 | "response": { 15 | "success": true, 16 | "result": { 17 | "location": "San Francisco, CA", 18 | "temperature": "18°C", 19 | "condition": "Partly cloudy", 20 | "humidity": "65%", 21 | "wind": "12 mph NW" 22 | }, 23 | "tool_name": "get_weather", 24 | "arguments": { 25 | "location": "San Francisco" 26 | } 27 | }, 28 | "execution_time": 0.45, 29 | "gui_state": { 30 | "connected": true, 31 | "server_path": "src/mcp_server/server.py", 32 | "available_tools": ["roll_dice", "get_weather", "get_date"] 33 | } 34 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/outputs/streamlit_roll_dice_interaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "interaction_type": "streamlit_gui", 3 | "tool_name": "roll_dice", 4 | "timestamp": "2024-01-15T10:30:00Z", 5 | "request": { 6 | "method": "tool_invocation", 7 | "parameters": { 8 | "tool": "roll_dice", 9 | "arguments": { 10 | "notation": "2d6" 11 | } 12 | } 13 | }, 14 | "response": { 15 | "success": true, 16 | "result": { 17 | "values": [3, 5], 18 | "total": 8, 19 | "notation": "2d6" 20 | }, 21 | "tool_name": "roll_dice", 22 | "arguments": { 23 | "notation": "2d6" 24 | } 25 | }, 26 | "execution_time": 0.15, 27 | "gui_state": { 28 | "connected": true, 29 | "server_path": "src/mcp_server/server.py", 30 | "available_tools": ["roll_dice", "get_weather", "get_date"] 31 | } 32 | } -------------------------------------------------------------------------------- /examples/mcp-server-client/context/templates/feature_base.md: -------------------------------------------------------------------------------- 1 | # Feature description for: [ Initial template for new features ] 2 | 3 | ## FEATURE 4 | 5 | [Insert your feature here] 6 | 7 | ## EXAMPLES 8 | 9 | [Provide and explain examples that you have in the `/context/examples` folder] 10 | 11 | ## DOCUMENTATION 12 | 13 | [List out any documentation (web pages, sources for an MCP server like Crawl4AI RAG, etc.) that will need to be referenced during development] 14 | 15 | ## OTHER CONSIDERATIONS 16 | 17 | [Any other considerations or specific requirements - great place to include gotchas that you see AI coding assistants miss with your projects a lot] 18 | -------------------------------------------------------------------------------- /examples/mcp-server-client/context/templates/prp_base.md: -------------------------------------------------------------------------------- 1 | # "Base PRP Template v2 - Context-Rich with Validation Loops" 2 | 3 | ## Purpose 4 | 5 | Product Requirements Prompt (PRP) Template optimized for AI agents to implement features with sufficient context and self-validation capabilities to achieve working code through iterative refinement. 6 | 7 | ## Core Principles 8 | 9 | 1. **Context is King**: Include ALL necessary documentation, examples, and caveats 10 | 2. **Validation Loops**: Provide executable tests/lints the AI can run and fix 11 | 3. **Information Dense**: Use keywords and patterns from the codebase 12 | 4. **Progressive Success**: Start simple, validate, then enhance 13 | 5. **Global rules**: Be sure to follow all rules in CLAUDE.md 14 | 15 | --- 16 | 17 | ## Goal 18 | 19 | [What needs to be built - be specific about the end state and desires] 20 | 21 | ## Why 22 | 23 | - [Business value and user impact] 24 | - [Integration with existing features] 25 | - [Problems this solves and for whom] 26 | 27 | ## What 28 | 29 | [User-visible behavior and technical requirements] 30 | 31 | ### Success Criteria 32 | 33 | - [ ] [Specific measurable outcomes] 34 | 35 | ## All Needed Context 36 | 37 | ### Documentation & References (list all context needed to implement the feature) 38 | 39 | ```yaml 40 | # MUST READ - Include these in your context window 41 | - url: [Official API docs URL] 42 | why: [Specific sections/methods you'll need] 43 | 44 | - file: [path/to/example.py] 45 | why: [Pattern to follow, gotchas to avoid] 46 | 47 | - doc: [Library documentation URL] 48 | section: [Specific section about common pitfalls] 49 | critical: [Key insight that prevents common errors] 50 | 51 | - docfile: [PRPs/ai_docs/file.md] 52 | why: [docs that the user has pasted in to the project] 53 | ``` 54 | 55 | ### Current Codebase tree (run `tree` in the root of the project) to get an overview of the codebase 56 | 57 | ```bash 58 | 59 | ``` 60 | 61 | ### Desired Codebase tree with files to be added and responsibility of file 62 | 63 | ```bash 64 | 65 | ``` 66 | 67 | ### Known Gotchas of our codebase & Library Quirks 68 | 69 | ```python 70 | # CRITICAL: [Library name] requires [specific setup] 71 | # Example: FastAPI requires async functions for endpoints 72 | # Example: This ORM doesn't support batch inserts over 1000 records 73 | # Example: We use pydantic v2 and 74 | ``` 75 | 76 | ## Implementation Blueprint 77 | 78 | ### Data models and structure 79 | 80 | Create the core data models, we ensure type safety and consistency. 81 | 82 | ```python 83 | Examples: 84 | - orm models 85 | - pydantic models 86 | - pydantic schemas 87 | - pydantic validators 88 | ``` 89 | 90 | ### list of tasks to be completed to fullfill the PRP in the order they should be completed 91 | 92 | ```yaml 93 | Task 1: 94 | MODIFY src/existing_module.py: 95 | - FIND pattern: "class OldImplementation" 96 | - INJECT after line containing "def __init__" 97 | - PRESERVE existing method signatures 98 | 99 | CREATE src/new_feature.py: 100 | - MIRROR pattern from: src/similar_feature.py 101 | - MODIFY class name and core logic 102 | - KEEP error handling pattern identical 103 | 104 | ...(...) 105 | 106 | Task N: 107 | ... 108 | 109 | ``` 110 | 111 | ### Per task pseudocode as needed added to each task 112 | 113 | ```python 114 | 115 | # Task 1 116 | # Pseudocode with CRITICAL details dont write entire code 117 | async def new_feature(param: str) -> Result: 118 | # PATTERN: Always validate input first (see src/validators.py) 119 | validated = validate_input(param) # raises ValidationError 120 | 121 | # GOTCHA: This library requires connection pooling 122 | async with get_connection() as conn: # see src/db/pool.py 123 | # PATTERN: Use existing retry decorator 124 | @retry(attempts=3, backoff=exponential) 125 | async def _inner(): 126 | # CRITICAL: API returns 429 if >10 req/sec 127 | await rate_limiter.acquire() 128 | return await external_api.call(validated) 129 | 130 | result = await _inner() 131 | 132 | # PATTERN: Standardized response format 133 | return format_response(result) # see src/utils/responses.py 134 | ``` 135 | 136 | ### Integration Points 137 | 138 | ```yaml 139 | DATABASE: 140 | - migration: "Add column 'feature_enabled' to users table" 141 | - index: "CREATE INDEX idx_feature_lookup ON users(feature_id)" 142 | 143 | CONFIG: 144 | - add to: config/settings.py 145 | - pattern: "FEATURE_TIMEOUT = int(os.getenv('FEATURE_TIMEOUT', '30'))" 146 | 147 | ROUTES: 148 | - add to: src/api/routes.py 149 | - pattern: "router.include_router(feature_router, prefix='/feature')" 150 | ``` 151 | 152 | ## Validation Loop 153 | 154 | ### Level 1: Syntax & Style 155 | 156 | ```bash 157 | # Run these FIRST - fix any errors before proceeding 158 | ruff check src/new_feature.py --fix # Auto-fix what's possible 159 | mypy src/new_feature.py # Type checking 160 | 161 | # Expected: No errors. If errors, READ the error and fix. 162 | ``` 163 | 164 | ### Level 2: Unit Tests each new feature/file/function use existing test patterns 165 | 166 | ```python 167 | # CREATE test_new_feature.py with these test cases: 168 | def test_happy_path(): 169 | """Basic functionality works""" 170 | result = new_feature("valid_input") 171 | assert result.status == "success" 172 | 173 | def test_validation_error(): 174 | """Invalid input raises ValidationError""" 175 | with pytest.raises(ValidationError): 176 | new_feature("") 177 | 178 | def test_external_api_timeout(): 179 | """Handles timeouts gracefully""" 180 | with mock.patch('external_api.call', side_effect=TimeoutError): 181 | result = new_feature("valid") 182 | assert result.status == "error" 183 | assert "timeout" in result.message 184 | ``` 185 | 186 | ```bash 187 | # Run and iterate until passing: 188 | uv run pytest test_new_feature.py -v 189 | # If failing: Read error, understand root cause, fix code, re-run (never mock to pass) 190 | ``` 191 | 192 | ### Level 3: Integration Test 193 | 194 | ```bash 195 | # Start the service 196 | uv run python -m src.main --dev 197 | 198 | # Test the endpoint 199 | curl -X POST http://localhost:8000/feature \ 200 | -H "Content-Type: application/json" \ 201 | -d '{"param": "test_value"}' 202 | 203 | # Expected: {"status": "success", "data": {...}} 204 | # If error: Check logs at logs/app.log for stack trace 205 | ``` 206 | 207 | ## Final validation Checklist 208 | 209 | - [ ] All tests pass: `uv run pytest tests/ -v` 210 | - [ ] No linting errors: `uv run ruff check src/` 211 | - [ ] No type errors: `uv run mypy src/` 212 | - [ ] Manual test successful: [specific curl/command] 213 | - [ ] Error cases handled gracefully 214 | - [ ] Logs are informative but not verbose 215 | - [ ] Documentation updated if needed 216 | 217 | --- 218 | 219 | ## Anti-Patterns to Avoid 220 | 221 | - ❌ Don't create new patterns when existing ones work 222 | - ❌ Don't skip validation because "it should work" 223 | - ❌ Don't ignore failing tests - fix them 224 | - ❌ Don't use sync functions in async context 225 | - ❌ Don't hardcode values that should be config 226 | - ❌ Don't catch all exceptions - be specific 227 | -------------------------------------------------------------------------------- /examples/mcp-server-client/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | version = "0.0.1" 7 | name = "context-engineering-template" 8 | description = "Assess the effectiveness of agentic AI systems across various use cases focusing on agnostic metrics that measure core agentic capabilities." 9 | authors = [ 10 | {name = "qte77", email = "qte@77.gh"} 11 | ] 12 | readme = "README.md" 13 | requires-python = "==3.13.*" 14 | license = "bsd-3-clause" 15 | dependencies = [ 16 | "mcp[cli]>=1.10.0", 17 | "httpx>=0.25.0", 18 | "pydantic>=2.0.0", 19 | "streamlit>=1.28.0", 20 | ] 21 | 22 | # [project.urls] 23 | # Documentation = "" 24 | 25 | [dependency-groups] 26 | dev = [ 27 | "mypy>=1.16.0", 28 | "ruff>=0.11.12", 29 | ] 30 | test = [ 31 | "pytest>=7.0.0", 32 | "pytest-asyncio>=0.21.0", 33 | "pytest-httpx>=0.28.0", 34 | ] 35 | docs = [ 36 | "griffe>=1.5.1", 37 | "mkdocs>=1.6.1", 38 | "mkdocs-awesome-pages-plugin>=2.9.3", 39 | "mkdocs-gen-files>=0.5.0", 40 | "mkdocs-literate-nav>=0.6.1", 41 | "mkdocs-material>=9.5.44", 42 | "mkdocs-section-index>=0.3.8", 43 | "mkdocstrings[python]>=0.27.0", 44 | ] 45 | 46 | [tool.uv] 47 | package = true 48 | exclude-newer = "2025-07-06T00:00:00Z" 49 | 50 | [tool.hatch.build.targets.wheel] 51 | only-include = ["/README.md"] 52 | 53 | [tool.hatch.build.targets.sdist] 54 | include = ["/README.md", "/Makefile", "/tests"] 55 | 56 | [tool.ruff] 57 | target-version = "py313" 58 | src = ["src", "tests"] 59 | 60 | [tool.ruff.format] 61 | docstring-code-format = true 62 | 63 | [tool.ruff.lint] 64 | # ignore = ["E203"] # Whitespace before ':' 65 | unfixable = ["B"] 66 | select = [ 67 | # pycodestyle 68 | "E", 69 | # Pyflakes 70 | "F", 71 | # pyupgrade 72 | "UP", 73 | # isort 74 | "I", 75 | ] 76 | 77 | [tool.ruff.lint.isort] 78 | known-first-party = ["src", "tests"] 79 | 80 | [tool.ruff.lint.pydocstyle] 81 | convention = "google" 82 | 83 | [tool.mypy] 84 | python_version = "3.13" 85 | strict = true 86 | disallow_untyped_defs = true 87 | disallow_any_generics = true 88 | warn_redundant_casts = true 89 | warn_unused_ignores = true 90 | warn_return_any = true 91 | warn_unreachable = true 92 | show_error_codes = true 93 | namespace_packages = true 94 | explicit_package_bases = true 95 | mypy_path = "src" 96 | 97 | [tool.pytest.ini_options] 98 | addopts = "--strict-markers" 99 | # "function", "class", "module", "package", "session" 100 | asyncio_default_fixture_loop_scope = "function" 101 | pythonpath = ["src"] 102 | testpaths = ["tests/"] 103 | 104 | [tool.coverage] 105 | [tool.coverage.run] 106 | include = [ 107 | "tests/**/*.py", 108 | ] 109 | # omit = [] 110 | # branch = true 111 | 112 | [tool.coverage.report] 113 | show_missing = true 114 | exclude_lines = [ 115 | # 'pragma: no cover', 116 | 'raise AssertionError', 117 | 'raise NotImplementedError', 118 | ] 119 | omit = [ 120 | 'env/*', 121 | 'venv/*', 122 | '.venv/*', 123 | '*/virtualenv/*', 124 | '*/virtualenvs/*', 125 | '*/tests/*', 126 | ] 127 | 128 | [tool.bumpversion] 129 | current_version = "0.0.1" 130 | parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" 131 | serialize = ["{major}.{minor}.{patch}"] 132 | commit = true 133 | tag = true 134 | allow_dirty = false 135 | ignore_missing_version = false 136 | sign_tags = false 137 | tag_name = "v{new_version}" 138 | tag_message = "Bump version: {current_version} → {new_version}" 139 | message = "Bump version: {current_version} → {new_version}" 140 | commit_args = "" 141 | 142 | [[tool.bumpversion.files]] 143 | filename = "pyproject.toml" 144 | search = 'version = "{current_version}"' 145 | replace = 'version = "{new_version}"' 146 | 147 | [[tool.bumpversion.files]] 148 | filename = "src/__init__.py" 149 | search = '__version__ = "{current_version}"' 150 | replace = '__version__ = "{new_version}"' 151 | 152 | [[tool.bumpversion.files]] 153 | filename = "README.md" 154 | search = "version-{current_version}-58f4c2" 155 | replace = "version-{new_version}-58f4c2" 156 | 157 | [[tool.bumpversion.files]] 158 | filename = "CHANGELOG.md" 159 | search = """ 160 | ## [Unreleased] 161 | """ 162 | replace = """ 163 | ## [Unreleased] 164 | 165 | ## [{new_version}] - {now:%Y-%m-%d} 166 | """ -------------------------------------------------------------------------------- /examples/mcp-server-client/src/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines the application version.""" 2 | 3 | __version__ = "0.0.1" 4 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """GUI module for MCP client interface.""" 2 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/app.py: -------------------------------------------------------------------------------- 1 | """Main Streamlit application for MCP Tool Showcase.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | import streamlit as st 7 | 8 | from src.gui.components.connection import ConnectionManager 9 | from src.gui.components.history import HistoryManager 10 | from src.gui.components.tool_forms import ToolForms 11 | from src.gui.models.gui_models import GUISession 12 | 13 | # Add project root to Python path 14 | project_root = Path(__file__).parent.parent.parent 15 | sys.path.insert(0, str(project_root)) 16 | 17 | 18 | def main() -> None: 19 | """Main Streamlit application entry point.""" 20 | st.set_page_config( 21 | page_title="MCP Tool Showcase", 22 | page_icon="🛠️", 23 | layout="wide", 24 | initial_sidebar_state="expanded", 25 | ) 26 | 27 | # Initialize session state 28 | if "gui_session" not in st.session_state: 29 | st.session_state.gui_session = GUISession() 30 | 31 | if "mcp_client" not in st.session_state: 32 | st.session_state.mcp_client = None 33 | 34 | # Main layout with sidebar 35 | with st.sidebar: 36 | st.title("🛠️ MCP Tool Showcase") 37 | st.markdown("---") 38 | 39 | # Connection management 40 | connection_manager = ConnectionManager() 41 | connection_manager.render() 42 | 43 | # Main content area 44 | col1, col2 = st.columns([1, 1]) 45 | 46 | with col1: 47 | st.header("Tool Invocation") 48 | if st.session_state.gui_session.connected: 49 | tool_forms = ToolForms() 50 | tool_forms.render() 51 | else: 52 | st.info("Please connect to the MCP server first") 53 | 54 | with col2: 55 | st.header("Request/Response") 56 | if st.session_state.gui_session.interaction_history: 57 | latest_interaction = st.session_state.gui_session.interaction_history[-1] 58 | st.subheader("Latest Request") 59 | st.json(latest_interaction.request_payload) 60 | st.subheader("Latest Response") 61 | st.json(latest_interaction.response_payload) 62 | else: 63 | st.info("No interactions yet") 64 | 65 | # History section 66 | st.header("Interaction History") 67 | history_manager = HistoryManager() 68 | history_manager.render() 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/components/__init__.py: -------------------------------------------------------------------------------- 1 | """GUI components for reusable interface elements.""" 2 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/components/connection.py: -------------------------------------------------------------------------------- 1 | """Connection management component for MCP server.""" 2 | 3 | import logging 4 | 5 | import streamlit as st 6 | 7 | from src.gui.utils.mcp_wrapper import MCPConnectionManager 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ConnectionManager: 13 | """Manages MCP server connection in GUI.""" 14 | 15 | def render(self) -> None: 16 | """Render connection management interface.""" 17 | st.subheader("Connection Status") 18 | 19 | # Display current status 20 | if st.session_state.gui_session.connected: 21 | st.success("✅ Connected to MCP Server") 22 | st.write(f"Server: {st.session_state.gui_session.server_path}") 23 | tools = ", ".join(st.session_state.gui_session.available_tools) 24 | st.write(f"Available Tools: {tools}") 25 | 26 | # Health check button 27 | if st.button("Health Check"): 28 | self._perform_health_check() 29 | 30 | # Disconnect button 31 | if st.button("Disconnect"): 32 | self._disconnect() 33 | else: 34 | st.error("❌ Not Connected") 35 | 36 | # Connection form 37 | server_path = st.text_input( 38 | "Server Path", 39 | value=st.session_state.gui_session.server_path, 40 | help="Path to MCP server script", 41 | ) 42 | 43 | if st.button("Connect"): 44 | self._connect(server_path) 45 | 46 | def _connect(self, server_path: str) -> None: 47 | """Connect to MCP server.""" 48 | try: 49 | with st.spinner("Connecting to server..."): 50 | # Create connection manager if it doesn't exist 51 | if "mcp_connection_manager" not in st.session_state: 52 | st.session_state.mcp_connection_manager = MCPConnectionManager() 53 | 54 | manager = st.session_state.mcp_connection_manager 55 | 56 | # Connect to server 57 | success = manager.connect(server_path) 58 | 59 | if success: 60 | st.session_state.gui_session.connected = True 61 | st.session_state.gui_session.server_path = server_path 62 | st.session_state.gui_session.available_tools = ( 63 | manager.available_tools 64 | ) 65 | 66 | st.success("Connected successfully!") 67 | st.rerun() 68 | else: 69 | st.error("Failed to connect to server") 70 | 71 | except Exception as e: 72 | logger.error(f"Connection failed: {e}") 73 | st.error(f"Connection failed: {str(e)}") 74 | 75 | def _disconnect(self) -> None: 76 | """Disconnect from MCP server.""" 77 | if "mcp_connection_manager" in st.session_state: 78 | try: 79 | manager = st.session_state.mcp_connection_manager 80 | manager.disconnect() 81 | 82 | st.session_state.gui_session.connected = False 83 | st.session_state.gui_session.available_tools = [] 84 | st.success("Disconnected successfully!") 85 | st.rerun() 86 | except Exception as e: 87 | logger.error(f"Disconnect failed: {e}") 88 | st.error(f"Disconnect failed: {str(e)}") 89 | 90 | def _perform_health_check(self) -> None: 91 | """Perform health check on connection.""" 92 | if "mcp_connection_manager" in st.session_state: 93 | try: 94 | manager = st.session_state.mcp_connection_manager 95 | health = manager.health_check() 96 | 97 | if health: 98 | st.success("Health check passed!") 99 | else: 100 | st.warning("Health check failed - connection may be unhealthy") 101 | except Exception as e: 102 | logger.error(f"Health check error: {e}") 103 | st.error(f"Health check error: {str(e)}") 104 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/components/history.py: -------------------------------------------------------------------------------- 1 | """History management component for interaction tracking.""" 2 | 3 | import streamlit as st 4 | 5 | from src.gui.models.gui_models import GUIInteraction 6 | 7 | 8 | class HistoryManager: 9 | """Manages session history display and interaction.""" 10 | 11 | def render(self) -> None: 12 | """Render interaction history interface.""" 13 | history = st.session_state.gui_session.interaction_history 14 | 15 | if not history: 16 | st.info( 17 | "No interactions yet. Connect to a server and use tools to see history." 18 | ) 19 | return 20 | 21 | # History controls 22 | col1, col2 = st.columns([1, 1]) 23 | 24 | with col1: 25 | st.write(f"**Total Interactions:** {len(history)}") 26 | 27 | with col2: 28 | if st.button("Clear History"): 29 | st.session_state.gui_session.interaction_history = [] 30 | st.rerun() 31 | 32 | # Display history 33 | for i, interaction in enumerate(reversed(history)): 34 | self._render_interaction(i, interaction) 35 | 36 | def _render_interaction(self, index: int, interaction: GUIInteraction) -> None: 37 | """Render a single interaction.""" 38 | # Create expander with status indicator 39 | status_icon = "✅" if interaction.success else "❌" 40 | timestamp = interaction.timestamp.strftime("%H:%M:%S") 41 | 42 | with st.expander( 43 | f"{status_icon} {interaction.tool_name} - {timestamp}", 44 | expanded=index == 0, # Expand latest interaction 45 | ): 46 | # Basic info 47 | col1, col2 = st.columns([1, 1]) 48 | 49 | with col1: 50 | st.write(f"**Tool:** {interaction.tool_name}") 51 | st.write( 52 | f"**Status:** {'Success' if interaction.success else 'Failed'}" 53 | ) 54 | if interaction.execution_time: 55 | st.write(f"**Execution Time:** {interaction.execution_time:.2f}s") 56 | 57 | with col2: 58 | timestamp_str = interaction.timestamp.strftime("%Y-%m-%d %H:%M:%S") 59 | st.write(f"**Timestamp:** {timestamp_str}") 60 | if interaction.error_message: 61 | st.write(f"**Error:** {interaction.error_message}") 62 | 63 | # Request/Response payloads 64 | req_col, resp_col = st.columns([1, 1]) 65 | 66 | with req_col: 67 | st.subheader("Request") 68 | st.json(interaction.request_payload) 69 | 70 | with resp_col: 71 | st.subheader("Response") 72 | st.json(interaction.response_payload) 73 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/components/tool_forms.py: -------------------------------------------------------------------------------- 1 | """Tool-specific form components for MCP tools.""" 2 | 3 | import logging 4 | import re 5 | import time 6 | 7 | import streamlit as st 8 | 9 | from src.gui.models.gui_models import GUIInteraction 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class ToolForms: 15 | """Manages tool-specific form interfaces.""" 16 | 17 | def render(self) -> None: 18 | """Render tool selection and forms.""" 19 | available_tools = st.session_state.gui_session.available_tools 20 | 21 | if not available_tools: 22 | st.warning("No tools available") 23 | return 24 | 25 | # Tool selection 26 | selected_tool = st.selectbox( 27 | "Select Tool", options=available_tools, help="Choose a tool to invoke" 28 | ) 29 | 30 | # Tool-specific forms 31 | if selected_tool == "roll_dice": 32 | self._render_dice_form() 33 | elif selected_tool == "get_weather": 34 | self._render_weather_form() 35 | elif selected_tool == "get_date": 36 | self._render_date_form() 37 | 38 | def _render_dice_form(self) -> None: 39 | """Render dice rolling form.""" 40 | with st.form("dice_form"): 41 | st.subheader("🎲 Roll Dice") 42 | 43 | notation = st.text_input( 44 | "Dice Notation", 45 | value="2d6", 46 | help="Enter dice notation (e.g., 2d6, 1d20, 3d10)", 47 | ) 48 | 49 | # Help text with examples 50 | st.caption("Examples: 1d20 (single 20-sided die), 3d6 (three 6-sided dice)") 51 | 52 | submitted = st.form_submit_button("Roll Dice") 53 | 54 | if submitted: 55 | if self._validate_dice_notation(notation): 56 | self._execute_tool("roll_dice", {"notation": notation}) 57 | else: 58 | st.error("Invalid dice notation. Use format like '2d6' or '1d20'") 59 | 60 | def _render_weather_form(self) -> None: 61 | """Render weather lookup form.""" 62 | with st.form("weather_form"): 63 | st.subheader("🌤️ Get Weather") 64 | 65 | location = st.text_input( 66 | "Location", 67 | value="San Francisco", 68 | help="Enter city name or coordinates (lat,lon)", 69 | ) 70 | 71 | # Common location examples 72 | st.caption("Examples: London, New York, 37.7749,-122.4194") 73 | 74 | submitted = st.form_submit_button("Get Weather") 75 | 76 | if submitted: 77 | if location.strip(): 78 | self._execute_tool("get_weather", {"location": location}) 79 | else: 80 | st.error("Please enter a location") 81 | 82 | def _render_date_form(self) -> None: 83 | """Render date/time lookup form.""" 84 | with st.form("date_form"): 85 | st.subheader("🕐 Get Date & Time") 86 | 87 | timezone = st.selectbox( 88 | "Timezone", 89 | options=[ 90 | "UTC", 91 | "America/New_York", 92 | "America/Los_Angeles", 93 | "Europe/London", 94 | "Asia/Tokyo", 95 | "Australia/Sydney", 96 | ], 97 | help="Select timezone or enter custom IANA timezone", 98 | ) 99 | 100 | custom_timezone = st.text_input( 101 | "Custom Timezone (optional)", 102 | placeholder="e.g., America/Chicago", 103 | help="Enter custom IANA timezone identifier", 104 | ) 105 | 106 | submitted = st.form_submit_button("Get Date & Time") 107 | 108 | if submitted: 109 | tz = custom_timezone.strip() if custom_timezone.strip() else timezone 110 | self._execute_tool("get_date", {"timezone": tz}) 111 | 112 | def _execute_tool(self, tool_name: str, arguments: dict[str, str]) -> None: 113 | """Execute tool and update GUI state.""" 114 | if "mcp_connection_manager" not in st.session_state: 115 | st.error("Not connected to server") 116 | return 117 | 118 | try: 119 | with st.spinner(f"Executing {tool_name}..."): 120 | # Time execution for performance metrics 121 | start_time = time.time() 122 | 123 | # Use connection manager 124 | manager = st.session_state.mcp_connection_manager 125 | result = manager.invoke_tool(tool_name, arguments) 126 | 127 | execution_time = time.time() - start_time 128 | 129 | # Create interaction record 130 | interaction = GUIInteraction( 131 | tool_name=tool_name, 132 | arguments=arguments, 133 | request_payload={"tool": tool_name, "arguments": arguments}, 134 | response_payload=result, 135 | success=result.get("success", False), 136 | error_message=result.get("error") 137 | if not result.get("success", False) 138 | else None, 139 | execution_time=execution_time, 140 | ) 141 | 142 | # Update session state 143 | st.session_state.gui_session.interaction_history.append(interaction) 144 | 145 | # Display result 146 | if result.get("success", False): 147 | st.success(f"✅ {tool_name} executed successfully!") 148 | st.json(result) 149 | else: 150 | st.error( 151 | f"❌ {tool_name} failed: {result.get('error', 'Unknown error')}" 152 | ) 153 | 154 | st.rerun() 155 | 156 | except Exception as e: 157 | logger.error(f"Tool execution error: {e}") 158 | st.error(f"Execution error: {str(e)}") 159 | 160 | def _validate_dice_notation(self, notation: str) -> bool: 161 | """Validate dice notation format.""" 162 | pattern = r"^(\d+)d(\d+)$" 163 | return bool(re.match(pattern, notation.strip().lower())) 164 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/models/__init__.py: -------------------------------------------------------------------------------- 1 | """GUI models for state management and data validation.""" 2 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/models/gui_models.py: -------------------------------------------------------------------------------- 1 | """GUI-specific models for state management and type safety.""" 2 | 3 | from datetime import datetime 4 | from typing import Any 5 | 6 | from pydantic import BaseModel, Field 7 | 8 | 9 | class GUIInteraction(BaseModel): 10 | """Single tool interaction record for GUI history.""" 11 | 12 | timestamp: datetime = Field(default_factory=datetime.now) 13 | tool_name: str 14 | arguments: dict[str, Any] 15 | request_payload: dict[str, Any] 16 | response_payload: dict[str, Any] 17 | success: bool 18 | error_message: str | None = None 19 | execution_time: float | None = None 20 | 21 | 22 | class GUISession(BaseModel): 23 | """GUI session state management.""" 24 | 25 | connected: bool = False 26 | server_path: str = "src/mcp_server/server.py" 27 | available_tools: list[str] = Field(default_factory=list) 28 | interaction_history: list[GUIInteraction] = Field(default_factory=list) 29 | current_tool: str | None = None 30 | 31 | 32 | class ConnectionStatus(BaseModel): 33 | """Connection status information.""" 34 | 35 | connected: bool = False 36 | server_path: str 37 | last_health_check: datetime | None = None 38 | available_tools: list[str] = Field(default_factory=list) 39 | error_message: str | None = None 40 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """GUI utilities for formatting and validation.""" 2 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/utils/async_helper.py: -------------------------------------------------------------------------------- 1 | """Async helper utilities for Streamlit GUI.""" 2 | 3 | import asyncio 4 | import threading 5 | from collections.abc import Awaitable 6 | from typing import TypeVar 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | def run_async(coro: Awaitable[T]) -> T: 12 | """Run an async function in Streamlit context. 13 | 14 | This handles the case where Streamlit might already have an event loop running. 15 | 16 | Args: 17 | coro: The coroutine to run 18 | 19 | Returns: 20 | The result of the coroutine 21 | """ 22 | try: 23 | # Try to get the current event loop 24 | # loop = asyncio.get_running_loop() 25 | 26 | # If we're already in an event loop, we need to run in a new thread 27 | # with its own event loop 28 | result = None 29 | exception = None 30 | 31 | def run_in_new_loop(): 32 | nonlocal result, exception 33 | try: 34 | new_loop = asyncio.new_event_loop() 35 | asyncio.set_event_loop(new_loop) 36 | try: 37 | result = new_loop.run_until_complete(coro) 38 | finally: 39 | new_loop.close() 40 | except Exception as e: 41 | exception = e 42 | 43 | thread = threading.Thread(target=run_in_new_loop) 44 | thread.start() 45 | thread.join() 46 | 47 | if exception: 48 | raise exception 49 | return result 50 | 51 | except RuntimeError: 52 | # No event loop running, can use asyncio.run directly 53 | return asyncio.run(coro) 54 | 55 | 56 | class AsyncContextManager: 57 | """Helper to manage async context across multiple calls.""" 58 | 59 | def __init__(self): 60 | self._client = None 61 | self._loop = None 62 | self._thread = None 63 | 64 | def run_async(self, coro: Awaitable[T]) -> T: 65 | """Run async function maintaining context.""" 66 | return run_async(coro) 67 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/utils/formatting.py: -------------------------------------------------------------------------------- 1 | """Formatting utilities for GUI display.""" 2 | 3 | import json 4 | from typing import Any 5 | 6 | 7 | def format_json_for_display(data: Any, indent: int = 2) -> str: 8 | """Format JSON data for display with proper indentation. 9 | 10 | Args: 11 | data: Data to format as JSON 12 | indent: Number of spaces for indentation 13 | 14 | Returns: 15 | Formatted JSON string 16 | """ 17 | try: 18 | return json.dumps(data, indent=indent, ensure_ascii=False) 19 | except (TypeError, ValueError) as e: 20 | return f"Error formatting JSON: {str(e)}" 21 | 22 | 23 | def format_error_message(error: str) -> str: 24 | """Format error messages for user-friendly display. 25 | 26 | Args: 27 | error: Raw error message 28 | 29 | Returns: 30 | Formatted error message 31 | """ 32 | # Remove common Python error prefixes 33 | error = error.replace("Exception: ", "") 34 | error = error.replace("Error: ", "") 35 | 36 | # Capitalize first letter 37 | if error: 38 | error = error[0].upper() + error[1:] 39 | 40 | return error 41 | 42 | 43 | def format_execution_time(seconds: float) -> str: 44 | """Format execution time for display. 45 | 46 | Args: 47 | seconds: Execution time in seconds 48 | 49 | Returns: 50 | Formatted time string 51 | """ 52 | if seconds < 1: 53 | return f"{seconds * 1000:.0f}ms" 54 | elif seconds < 60: 55 | return f"{seconds:.2f}s" 56 | else: 57 | minutes = int(seconds // 60) 58 | remaining_seconds = seconds % 60 59 | return f"{minutes}m {remaining_seconds:.1f}s" 60 | 61 | 62 | def truncate_text(text: str, max_length: int = 100) -> str: 63 | """Truncate text with ellipsis if too long. 64 | 65 | Args: 66 | text: Text to truncate 67 | max_length: Maximum length before truncation 68 | 69 | Returns: 70 | Truncated text with ellipsis if needed 71 | """ 72 | if len(text) <= max_length: 73 | return text 74 | return text[: max_length - 3] + "..." 75 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/utils/mcp_wrapper.py: -------------------------------------------------------------------------------- 1 | """MCP client wrapper for Streamlit GUI with persistent connection.""" 2 | 3 | import asyncio 4 | import logging 5 | import threading 6 | import time 7 | from queue import Empty, Queue 8 | from typing import Any 9 | 10 | from src.mcp_client.client import MCPClient 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class MCPConnectionManager: 16 | """Manages a persistent MCP connection in a background thread.""" 17 | 18 | def __init__(self): 19 | self._client = None 20 | self._thread = None 21 | self._loop = None 22 | self._connected = False 23 | self._available_tools = [] 24 | self._request_queue = Queue() 25 | self._response_queue = Queue() 26 | self._shutdown_event = threading.Event() 27 | 28 | def connect(self, server_path: str) -> bool: 29 | """Connect to MCP server in background thread.""" 30 | if self._connected: 31 | self.disconnect() 32 | 33 | self._shutdown_event.clear() 34 | self._thread = threading.Thread( 35 | target=self._run_connection, args=(server_path,) 36 | ) 37 | self._thread.daemon = True 38 | self._thread.start() 39 | 40 | # Wait for connection to establish (with timeout) 41 | start_time = time.time() 42 | while not self._connected and time.time() - start_time < 10: 43 | time.sleep(0.1) 44 | 45 | return self._connected 46 | 47 | def disconnect(self) -> None: 48 | """Disconnect from MCP server.""" 49 | if self._thread and self._thread.is_alive(): 50 | self._shutdown_event.set() 51 | # Send disconnect command 52 | self._request_queue.put({"action": "disconnect"}) 53 | self._thread.join(timeout=5) 54 | 55 | self._connected = False 56 | self._available_tools = [] 57 | 58 | def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: 59 | """Invoke a tool and return the result.""" 60 | if not self._connected: 61 | return { 62 | "success": False, 63 | "error": "Not connected to server", 64 | "tool_name": tool_name, 65 | "arguments": arguments, 66 | } 67 | 68 | # Send request 69 | request_id = f"{tool_name}_{time.time()}" 70 | self._request_queue.put( 71 | { 72 | "action": "invoke_tool", 73 | "id": request_id, 74 | "tool_name": tool_name, 75 | "arguments": arguments, 76 | } 77 | ) 78 | 79 | # Wait for response (with timeout) 80 | start_time = time.time() 81 | while time.time() - start_time < 30: # 30 second timeout 82 | try: 83 | response = self._response_queue.get(timeout=1) 84 | if response.get("id") == request_id: 85 | return response.get( 86 | "result", 87 | { 88 | "success": False, 89 | "error": "No result in response", 90 | "tool_name": tool_name, 91 | "arguments": arguments, 92 | }, 93 | ) 94 | except Empty: 95 | continue 96 | 97 | return { 98 | "success": False, 99 | "error": "Request timeout", 100 | "tool_name": tool_name, 101 | "arguments": arguments, 102 | } 103 | 104 | def health_check(self) -> bool: 105 | """Check if connection is healthy.""" 106 | if not self._connected: 107 | return False 108 | 109 | # Send health check request 110 | request_id = f"health_{time.time()}" 111 | self._request_queue.put({"action": "health_check", "id": request_id}) 112 | 113 | # Wait for response 114 | start_time = time.time() 115 | while time.time() - start_time < 5: # 5 second timeout 116 | try: 117 | response = self._response_queue.get(timeout=1) 118 | if response.get("id") == request_id: 119 | return response.get("result", False) 120 | except Empty: 121 | continue 122 | 123 | return False 124 | 125 | @property 126 | def connected(self) -> bool: 127 | """Check if connected.""" 128 | return self._connected 129 | 130 | @property 131 | def available_tools(self) -> list[str]: 132 | """Get available tools.""" 133 | return self._available_tools.copy() 134 | 135 | def _run_connection(self, server_path: str) -> None: 136 | """Run the connection in background thread with its own event loop.""" 137 | try: 138 | # Create new event loop for this thread 139 | self._loop = asyncio.new_event_loop() 140 | asyncio.set_event_loop(self._loop) 141 | 142 | # Run the connection handler 143 | self._loop.run_until_complete(self._connection_handler(server_path)) 144 | 145 | except Exception as e: 146 | logger.error(f"Connection thread error: {e}") 147 | finally: 148 | if self._loop: 149 | self._loop.close() 150 | self._connected = False 151 | 152 | async def _connection_handler(self, server_path: str) -> None: 153 | """Handle the MCP connection and requests.""" 154 | try: 155 | # Create and connect client 156 | self._client = MCPClient(server_path) 157 | await self._client.connect() 158 | 159 | self._connected = True 160 | self._available_tools = self._client.available_tools.copy() 161 | logger.info( 162 | f"Connected to MCP server. Available tools: {self._available_tools}" 163 | ) 164 | 165 | # Process requests until shutdown 166 | while not self._shutdown_event.is_set(): 167 | try: 168 | # Check for requests (non-blocking) 169 | request = self._request_queue.get(timeout=0.1) 170 | 171 | if request["action"] == "disconnect": 172 | break 173 | elif request["action"] == "invoke_tool": 174 | await self._handle_invoke_tool(request) 175 | elif request["action"] == "health_check": 176 | await self._handle_health_check(request) 177 | 178 | except Empty: 179 | # No request, continue 180 | continue 181 | except Exception as e: 182 | logger.error(f"Error processing request: {e}") 183 | 184 | except Exception as e: 185 | logger.error(f"Connection handler error: {e}") 186 | self._response_queue.put( 187 | { 188 | "id": "connection_error", 189 | "result": {"success": False, "error": str(e)}, 190 | } 191 | ) 192 | finally: 193 | # Clean up 194 | if self._client: 195 | try: 196 | await self._client.disconnect() 197 | except Exception as e: 198 | logger.error(f"Error disconnecting client: {e}") 199 | self._connected = False 200 | 201 | async def _handle_invoke_tool(self, request: dict[str, Any]) -> None: 202 | """Handle tool invocation request.""" 203 | try: 204 | result = await self._client.invoke_tool( 205 | request["tool_name"], request["arguments"] 206 | ) 207 | 208 | self._response_queue.put( 209 | { 210 | "id": request["id"], 211 | "result": result.model_dump() 212 | if hasattr(result, "model_dump") 213 | else { 214 | "success": True, 215 | "result": result, 216 | "tool_name": request["tool_name"], 217 | "arguments": request["arguments"], 218 | }, 219 | } 220 | ) 221 | 222 | except Exception as e: 223 | logger.error(f"Tool invocation error: {e}") 224 | self._response_queue.put( 225 | { 226 | "id": request["id"], 227 | "result": { 228 | "success": False, 229 | "error": str(e), 230 | "tool_name": request["tool_name"], 231 | "arguments": request["arguments"], 232 | }, 233 | } 234 | ) 235 | 236 | async def _handle_health_check(self, request: dict[str, Any]) -> None: 237 | """Handle health check request.""" 238 | try: 239 | health = await self._client.health_check() 240 | self._response_queue.put({"id": request["id"], "result": health}) 241 | except Exception as e: 242 | logger.error(f"Health check error: {e}") 243 | self._response_queue.put({"id": request["id"], "result": False}) 244 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/gui/utils/validation.py: -------------------------------------------------------------------------------- 1 | """Validation utilities for GUI input.""" 2 | 3 | import re 4 | 5 | 6 | def validate_dice_notation(notation: str) -> bool: 7 | """Validate dice notation format. 8 | 9 | Args: 10 | notation: Dice notation string (e.g., "2d6", "1d20") 11 | 12 | Returns: 13 | True if valid, False otherwise 14 | """ 15 | if not notation or not isinstance(notation, str): 16 | return False 17 | 18 | # Pattern: number + 'd' + number 19 | pattern = r"^(\d+)d(\d+)$" 20 | match = re.match(pattern, notation.strip().lower()) 21 | 22 | if not match: 23 | return False 24 | 25 | # Check reasonable limits 26 | num_dice = int(match.group(1)) 27 | num_sides = int(match.group(2)) 28 | 29 | # Validate ranges 30 | if num_dice < 1 or num_dice > 100: 31 | return False 32 | if num_sides < 2 or num_sides > 1000: 33 | return False 34 | 35 | return True 36 | 37 | 38 | def validate_location(location: str) -> bool: 39 | """Validate location input for weather lookup. 40 | 41 | Args: 42 | location: Location string (city name or coordinates) 43 | 44 | Returns: 45 | True if valid, False otherwise 46 | """ 47 | if not location or not isinstance(location, str): 48 | return False 49 | 50 | location = location.strip() 51 | 52 | # Check for coordinates format (lat,lon) 53 | coord_pattern = r"^-?\d+\.?\d*,-?\d+\.?\d*$" 54 | if re.match(coord_pattern, location): 55 | return True 56 | 57 | # Check for city name (at least 2 characters, letters and spaces) 58 | city_pattern = r"^[a-zA-Z\s]{2,}$" 59 | if re.match(city_pattern, location): 60 | return True 61 | 62 | return False 63 | 64 | 65 | def validate_timezone(timezone: str) -> bool: 66 | """Validate timezone identifier. 67 | 68 | Args: 69 | timezone: Timezone string (IANA format) 70 | 71 | Returns: 72 | True if valid, False otherwise 73 | """ 74 | if not timezone or not isinstance(timezone, str): 75 | return False 76 | 77 | timezone = timezone.strip() 78 | 79 | # Common timezone patterns 80 | common_timezones = [ 81 | "UTC", 82 | "GMT", 83 | "EST", 84 | "PST", 85 | "CST", 86 | "MST", 87 | "America/New_York", 88 | "America/Los_Angeles", 89 | "America/Chicago", 90 | "Europe/London", 91 | "Europe/Paris", 92 | "Asia/Tokyo", 93 | "Australia/Sydney", 94 | ] 95 | 96 | if timezone in common_timezones: 97 | return True 98 | 99 | # IANA timezone format: Area/City or Area/Region/City 100 | iana_pattern = r"^[A-Za-z_]+/[A-Za-z_]+(?:/[A-Za-z_]+)?$" 101 | return bool(re.match(iana_pattern, timezone)) 102 | 103 | 104 | def validate_server_path(path: str) -> str | None: 105 | """Validate server path and return error message if invalid. 106 | 107 | Args: 108 | path: Server path string 109 | 110 | Returns: 111 | Error message if invalid, None if valid 112 | """ 113 | if not path or not isinstance(path, str): 114 | return "Server path is required" 115 | 116 | path = path.strip() 117 | 118 | if not path: 119 | return "Server path cannot be empty" 120 | 121 | if not path.endswith(".py"): 122 | return "Server path must be a Python file (.py)" 123 | 124 | # Basic path validation 125 | if ".." in path or path.startswith("/"): 126 | return "Server path should be relative to project root" 127 | 128 | return None 129 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/main.py: -------------------------------------------------------------------------------- 1 | """Main entry point for the MCP server and client applications.""" 2 | 3 | import argparse 4 | import logging 5 | import sys 6 | from asyncio import run 7 | 8 | from src.mcp_client.cli import MCPClientCLI 9 | from src.mcp_server import run_server 10 | 11 | 12 | def setup_logging(level: str = "INFO") -> None: 13 | """Setup logging configuration.""" 14 | logging.basicConfig( 15 | level=getattr(logging, level.upper()), 16 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 17 | datefmt="%Y-%m-%d %H:%M:%S", 18 | ) 19 | 20 | 21 | async def run_client_async(args) -> int: 22 | """Run the MCP client in async mode.""" 23 | # Create client CLI and run it 24 | cli = MCPClientCLI() 25 | 26 | # Build client arguments from parsed args 27 | client_args = [ 28 | "--server", 29 | args.server, 30 | "--log-level", 31 | args.log_level, 32 | "--timeout", 33 | str(args.timeout), 34 | ] 35 | 36 | if args.tool: 37 | client_args.append(args.tool) 38 | 39 | # Add tool-specific arguments 40 | if args.tool == "roll_dice": 41 | client_args.extend(["--notation", args.notation]) 42 | elif args.tool == "get_weather": 43 | client_args.extend(["--location", args.location]) 44 | elif args.tool == "get_date": 45 | client_args.extend(["--timezone", args.timezone]) 46 | 47 | # Run the client 48 | return await cli.run(client_args) 49 | 50 | 51 | def main() -> None: 52 | """Main entry point for MCP applications.""" 53 | parser = argparse.ArgumentParser( 54 | description="MCP Server and Client with dice, weather, and date/time tools", 55 | formatter_class=argparse.RawDescriptionHelpFormatter, 56 | epilog=""" 57 | Examples: 58 | # Run MCP server 59 | %(prog)s server 60 | 61 | # Run MCP client 62 | %(prog)s client --server ./server.py roll_dice --notation 2d6 63 | %(prog)s client --server ./server.py get_weather --location "San Francisco" 64 | %(prog)s client --server ./server.py get_date --timezone UTC 65 | 66 | # Launch Streamlit GUI 67 | %(prog)s gui 68 | %(prog)s gui --port 8502 69 | """, 70 | ) 71 | 72 | # Add subcommands 73 | subparsers = parser.add_subparsers( 74 | dest="mode", help="Choose server or client mode", metavar="MODE" 75 | ) 76 | 77 | # Server subcommand 78 | server_parser = subparsers.add_parser("server", help="Run MCP server") 79 | server_parser.add_argument( 80 | "--log-level", 81 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 82 | default="INFO", 83 | help="Set the logging level", 84 | ) 85 | server_parser.add_argument( 86 | "--version", 87 | action="version", 88 | version="MCP Server v1.0.0", 89 | ) 90 | 91 | # Client subcommand 92 | client_parser = subparsers.add_parser("client", help="Run MCP client") 93 | 94 | # GUI subcommand 95 | gui_parser = subparsers.add_parser("gui", help="Launch Streamlit GUI") 96 | gui_parser.add_argument( 97 | "--log-level", 98 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 99 | default="INFO", 100 | help="Set the logging level", 101 | ) 102 | gui_parser.add_argument( 103 | "--port", 104 | type=int, 105 | default=8501, 106 | help="Port for Streamlit server (default: 8501)", 107 | ) 108 | client_parser.add_argument( 109 | "--server", required=True, help="Path to MCP server script" 110 | ) 111 | client_parser.add_argument( 112 | "--log-level", 113 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 114 | default="INFO", 115 | help="Set logging level (default: INFO)", 116 | ) 117 | client_parser.add_argument( 118 | "--timeout", 119 | type=int, 120 | default=30, 121 | help="Connection timeout in seconds (default: 30)", 122 | ) 123 | 124 | # Tool subcommands for client 125 | tool_subparsers = client_parser.add_subparsers( 126 | dest="tool", help="Available tools", metavar="TOOL" 127 | ) 128 | 129 | # Roll dice tool 130 | dice_parser = tool_subparsers.add_parser( 131 | "roll_dice", help="Roll dice using standard notation" 132 | ) 133 | dice_parser.add_argument( 134 | "--notation", required=True, help="Dice notation (e.g., 2d6, 1d20, 3d10)" 135 | ) 136 | 137 | # Weather tool 138 | weather_parser = tool_subparsers.add_parser( 139 | "get_weather", help="Get current weather conditions" 140 | ) 141 | weather_parser.add_argument( 142 | "--location", required=True, help="Location name or coordinates (lat,lon)" 143 | ) 144 | 145 | # Date/time tool 146 | date_parser = tool_subparsers.add_parser( 147 | "get_date", help="Get current date and time" 148 | ) 149 | date_parser.add_argument( 150 | "--timezone", default="UTC", help="Timezone identifier (default: UTC)" 151 | ) 152 | 153 | args = parser.parse_args() 154 | 155 | # Check if mode is specified 156 | if not args.mode: 157 | parser.print_help() 158 | sys.exit(1) 159 | 160 | # Setup logging 161 | log_level = getattr(args, "log_level", "INFO") 162 | setup_logging(log_level) 163 | 164 | logger = logging.getLogger(__name__) 165 | 166 | try: 167 | if args.mode == "server": 168 | logger.info("Starting MCP Server application") 169 | # Run the MCP server (this will block until server shuts down) 170 | run_server() 171 | elif args.mode == "client": 172 | logger.info("Starting MCP Client application") 173 | # Run client in async mode 174 | exit_code = run(run_client_async(args)) 175 | sys.exit(exit_code) 176 | elif args.mode == "gui": 177 | logger.info("Starting Streamlit GUI application") 178 | # Launch Streamlit GUI 179 | import os 180 | import subprocess 181 | 182 | # Set environment variables for Streamlit 183 | env = os.environ.copy() 184 | env["STREAMLIT_SERVER_PORT"] = str(args.port) 185 | env["STREAMLIT_LOGGER_LEVEL"] = args.log_level 186 | 187 | # Run Streamlit 188 | cmd = [ 189 | "streamlit", 190 | "run", 191 | "src/gui/app.py", 192 | "--server.port", 193 | str(args.port), 194 | "--logger.level", 195 | args.log_level.lower(), 196 | ] 197 | 198 | subprocess.run(cmd, env=env) 199 | else: 200 | parser.print_help() 201 | sys.exit(1) 202 | 203 | except KeyboardInterrupt: 204 | logger.info("Application interrupted by user") 205 | sys.exit(0) 206 | except Exception as e: 207 | logger.error(f"Application error: {e}") 208 | sys.exit(1) 209 | 210 | 211 | if __name__ == "__main__": 212 | main() 213 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP Client implementation for tool invocation.""" 2 | 3 | from .cli import MCPClientCLI 4 | from .client import MCPClient 5 | 6 | __all__ = ["MCPClient", "MCPClientCLI"] 7 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/cli.py: -------------------------------------------------------------------------------- 1 | """CLI interface for MCP client tool invocation.""" 2 | 3 | import argparse 4 | import asyncio 5 | import json 6 | import logging 7 | import sys 8 | from typing import Any 9 | 10 | from .client import MCPClient 11 | from .models.responses import ClientToolResult 12 | 13 | # Configure logging 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class MCPClientCLI: 18 | """CLI interface for MCP client tool invocation.""" 19 | 20 | def __init__(self) -> None: 21 | """Initialize CLI interface.""" 22 | self.parser = self._create_parser() 23 | self.client: MCPClient | None = None 24 | 25 | def _create_parser(self) -> argparse.ArgumentParser: 26 | """Create argument parser with subcommands. 27 | 28 | Returns: 29 | Configured argument parser 30 | """ 31 | parser = argparse.ArgumentParser( 32 | description="MCP Client for tool invocation", 33 | formatter_class=argparse.RawDescriptionHelpFormatter, 34 | epilog="""Examples: 35 | %(prog)s --server ./server.py roll_dice --notation 2d6 36 | %(prog)s --server ./server.py get_weather --location "San Francisco" 37 | %(prog)s --server ./server.py get_date --timezone UTC 38 | """, 39 | ) 40 | 41 | # Global arguments 42 | parser.add_argument("--server", required=True, help="Path to MCP server script") 43 | 44 | parser.add_argument( 45 | "--log-level", 46 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 47 | default="INFO", 48 | help="Set logging level (default: INFO)", 49 | ) 50 | 51 | parser.add_argument( 52 | "--timeout", 53 | type=int, 54 | default=30, 55 | help="Connection timeout in seconds (default: 30)", 56 | ) 57 | 58 | # Subcommands for tools 59 | subparsers = parser.add_subparsers( 60 | dest="tool", help="Available tools", metavar="TOOL" 61 | ) 62 | 63 | # Roll dice tool 64 | dice_parser = subparsers.add_parser( 65 | "roll_dice", help="Roll dice using standard notation" 66 | ) 67 | dice_parser.add_argument( 68 | "--notation", required=True, help="Dice notation (e.g., 2d6, 1d20, 3d10)" 69 | ) 70 | 71 | # Weather tool 72 | weather_parser = subparsers.add_parser( 73 | "get_weather", help="Get current weather conditions" 74 | ) 75 | weather_parser.add_argument( 76 | "--location", required=True, help="Location name or coordinates (lat,lon)" 77 | ) 78 | 79 | # Date/time tool 80 | date_parser = subparsers.add_parser( 81 | "get_date", help="Get current date and time" 82 | ) 83 | date_parser.add_argument( 84 | "--timezone", default="UTC", help="Timezone identifier (default: UTC)" 85 | ) 86 | 87 | return parser 88 | 89 | def _setup_logging(self, level: str) -> None: 90 | """Setup logging configuration. 91 | 92 | Args: 93 | level: Logging level string 94 | """ 95 | logging.basicConfig( 96 | level=getattr(logging, level.upper()), 97 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 98 | datefmt="%Y-%m-%d %H:%M:%S", 99 | ) 100 | 101 | def _build_tool_arguments(self, args: argparse.Namespace) -> dict[str, Any]: 102 | """Build tool arguments from parsed CLI arguments. 103 | 104 | Args: 105 | args: Parsed command line arguments 106 | 107 | Returns: 108 | Dictionary of tool arguments 109 | """ 110 | if args.tool == "roll_dice": 111 | return {"notation": args.notation} 112 | elif args.tool == "get_weather": 113 | return {"location": args.location} 114 | elif args.tool == "get_date": 115 | return {"timezone": args.timezone} 116 | else: 117 | return {} 118 | 119 | def _display_success(self, result: ClientToolResult) -> None: 120 | """Display successful tool result. 121 | 122 | Args: 123 | result: Successful tool result 124 | """ 125 | print(f"✅ {result.tool_name} executed successfully:") 126 | print() 127 | 128 | # Handle MCP response format 129 | if result.result and hasattr(result.result, "content"): 130 | content = result.result.content 131 | if isinstance(content, list): 132 | for item in content: 133 | if isinstance(item, dict) and "text" in item: 134 | print(item["text"]) 135 | else: 136 | print(json.dumps(item, indent=2)) 137 | else: 138 | print(json.dumps(content, indent=2)) 139 | else: 140 | print(json.dumps(result.result, indent=2)) 141 | 142 | def _display_error(self, result: ClientToolResult) -> None: 143 | """Display tool execution error. 144 | 145 | Args: 146 | result: Failed tool result 147 | """ 148 | print(f"❌ {result.tool_name} failed:") 149 | print(f"Error: {result.error}") 150 | print() 151 | print(f"Tool: {result.tool_name}") 152 | print(f"Arguments: {json.dumps(result.arguments, indent=2)}") 153 | 154 | def _display_connection_error(self, error: Exception) -> None: 155 | """Display connection error. 156 | 157 | Args: 158 | error: Connection error 159 | """ 160 | print("❌ Connection failed:") 161 | print(f"Error: {error}") 162 | print() 163 | print("Troubleshooting:") 164 | print("1. Check that the server script exists") 165 | print("2. Verify the server script is executable") 166 | print("3. Ensure required dependencies are installed") 167 | print("4. Check that the server starts correctly") 168 | 169 | async def run(self, args: list[str] | None = None) -> int: 170 | """Run CLI with provided arguments. 171 | 172 | Args: 173 | args: Command line arguments (uses sys.argv if None) 174 | 175 | Returns: 176 | Exit code (0 for success, 1 for error) 177 | """ 178 | try: 179 | # Parse arguments 180 | parsed_args = self.parser.parse_args(args) 181 | 182 | # Setup logging 183 | self._setup_logging(parsed_args.log_level) 184 | 185 | # Check if tool specified 186 | if not parsed_args.tool: 187 | self.parser.print_help() 188 | return 1 189 | 190 | # Create client 191 | self.client = MCPClient(parsed_args.server) 192 | 193 | # Connect to server with timeout 194 | try: 195 | await asyncio.wait_for( 196 | self.client.connect(), timeout=parsed_args.timeout 197 | ) 198 | except TimeoutError: 199 | print(f"❌ Connection timeout after {parsed_args.timeout} seconds") 200 | return 1 201 | except Exception as e: 202 | self._display_connection_error(e) 203 | return 1 204 | 205 | # Build tool arguments 206 | tool_args = self._build_tool_arguments(parsed_args) 207 | 208 | # Invoke tool 209 | result = await self.client.invoke_tool(parsed_args.tool, tool_args) 210 | 211 | # Display result 212 | if result.success: 213 | self._display_success(result) 214 | return 0 215 | else: 216 | self._display_error(result) 217 | return 1 218 | 219 | except KeyboardInterrupt: 220 | print("\n❌ Interrupted by user") 221 | return 130 222 | except Exception as e: 223 | logger.error(f"Unexpected error: {e}") 224 | print(f"❌ Unexpected error: {e}") 225 | return 1 226 | finally: 227 | # Cleanup 228 | if self.client: 229 | await self.client.disconnect() 230 | 231 | 232 | async def main() -> int: 233 | """Main CLI entry point. 234 | 235 | Returns: 236 | Exit code 237 | """ 238 | cli = MCPClientCLI() 239 | return await cli.run() 240 | 241 | 242 | if __name__ == "__main__": 243 | sys.exit(asyncio.run(main())) 244 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/client.py: -------------------------------------------------------------------------------- 1 | """Main MCP client class for tool invocation.""" 2 | 3 | import logging 4 | from typing import Any 5 | 6 | from .models.responses import ClientToolResult 7 | from .transport import MCPTransport 8 | 9 | # Configure logging 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class MCPClient: 14 | """MCP client for connecting to servers and invoking tools.""" 15 | 16 | def __init__(self, server_path: str): 17 | """Initialize MCP client. 18 | 19 | Args: 20 | server_path: Path to the MCP server script 21 | """ 22 | self.server_path = server_path 23 | self.transport = MCPTransport(server_path) 24 | self._connected = False 25 | 26 | @property 27 | def connected(self) -> bool: 28 | """Check if client is connected to server.""" 29 | return self._connected and self.transport.connected 30 | 31 | @property 32 | def available_tools(self) -> list[str]: 33 | """Get list of available tools.""" 34 | return self.transport.available_tools 35 | 36 | async def connect(self) -> None: 37 | """Connect to MCP server. 38 | 39 | Raises: 40 | FileNotFoundError: If server script doesn't exist 41 | ConnectionError: If connection fails 42 | ValueError: If server script type is not supported 43 | """ 44 | logger.info(f"Connecting to MCP server: {self.server_path}") 45 | 46 | try: 47 | await self.transport.connect() 48 | self._connected = True 49 | logger.info( 50 | f"Connected successfully. Available tools: {self.available_tools}" 51 | ) 52 | except Exception as e: 53 | logger.error(f"Failed to connect: {e}") 54 | raise 55 | 56 | async def disconnect(self) -> None: 57 | """Disconnect from MCP server.""" 58 | logger.info("Disconnecting from MCP server") 59 | await self.transport.disconnect() 60 | self._connected = False 61 | 62 | async def invoke_tool( 63 | self, tool_name: str, arguments: dict[str, Any] 64 | ) -> ClientToolResult: 65 | """Invoke a tool on the connected server. 66 | 67 | Args: 68 | tool_name: Name of the tool to invoke 69 | arguments: Arguments to pass to the tool 70 | 71 | Returns: 72 | ClientToolResult with success status and result or error 73 | """ 74 | logger.info(f"Invoking tool: {tool_name} with arguments: {arguments}") 75 | 76 | # Check if connected 77 | if not self.connected: 78 | error_msg = "Not connected to server" 79 | logger.error(error_msg) 80 | return ClientToolResult( 81 | success=False, 82 | result=None, 83 | error=error_msg, 84 | tool_name=tool_name, 85 | arguments=arguments, 86 | ) 87 | 88 | # Check if tool is available 89 | if tool_name not in self.available_tools: 90 | error_msg = ( 91 | f"Tool '{tool_name}' not available. " 92 | f"Available tools: {self.available_tools}" 93 | ) 94 | logger.error(error_msg) 95 | return ClientToolResult( 96 | success=False, 97 | result=None, 98 | error=error_msg, 99 | tool_name=tool_name, 100 | arguments=arguments, 101 | ) 102 | 103 | try: 104 | # Call the tool through transport 105 | result = await self.transport.call_tool(tool_name, arguments) 106 | 107 | # Process the result 108 | logger.info(f"Tool '{tool_name}' executed successfully") 109 | return ClientToolResult( 110 | success=True, 111 | result=result, 112 | error=None, 113 | tool_name=tool_name, 114 | arguments=arguments, 115 | ) 116 | 117 | except Exception as e: 118 | error_msg = f"Tool execution failed: {str(e)}" 119 | logger.error(error_msg) 120 | return ClientToolResult( 121 | success=False, 122 | result=None, 123 | error=error_msg, 124 | tool_name=tool_name, 125 | arguments=arguments, 126 | ) 127 | 128 | async def health_check(self) -> bool: 129 | """Check if connection is healthy. 130 | 131 | Returns: 132 | True if connection is healthy, False otherwise 133 | """ 134 | if not self.connected: 135 | return False 136 | 137 | return await self.transport.health_check() 138 | 139 | async def __aenter__(self) -> "MCPClient": 140 | """Async context manager entry.""" 141 | await self.connect() 142 | return self 143 | 144 | async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 145 | """Async context manager exit.""" 146 | await self.disconnect() 147 | 148 | 149 | class MCPClientError(Exception): 150 | """Base exception for MCP client errors.""" 151 | 152 | pass 153 | 154 | 155 | class MCPConnectionError(MCPClientError): 156 | """Raised when connection to MCP server fails.""" 157 | 158 | pass 159 | 160 | 161 | class MCPToolError(MCPClientError): 162 | """Raised when tool execution fails.""" 163 | 164 | pass 165 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/models/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP Client models for type safety and validation.""" 2 | 3 | from .responses import ClientToolResult, MCPToolResponse 4 | 5 | __all__ = ["ClientToolResult", "MCPToolResponse"] 6 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/models/responses.py: -------------------------------------------------------------------------------- 1 | """Client-specific response models for MCP tool invocation.""" 2 | 3 | from typing import Any 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class MCPToolResponse(BaseModel): 9 | """Response from MCP tool execution.""" 10 | 11 | content: list[dict[str, Any]] = Field(..., description="Response content") 12 | isError: bool = Field(False, description="Whether response indicates error") 13 | 14 | 15 | class ClientToolResult(BaseModel): 16 | """Processed tool result for client consumption.""" 17 | 18 | success: bool = Field(..., description="Whether tool execution was successful") 19 | result: Any | None = Field(None, description="Tool execution result") 20 | error: str | None = Field(None, description="Error message if execution failed") 21 | tool_name: str = Field(..., description="Name of the tool that was executed") 22 | arguments: dict[str, Any] = Field(..., description="Arguments passed to the tool") 23 | 24 | 25 | class ClientSession(BaseModel): 26 | """Client session information.""" 27 | 28 | server_path: str = Field(..., description="Path to the MCP server script") 29 | connected: bool = Field(False, description="Whether client is connected to server") 30 | available_tools: list[str] = Field( 31 | default_factory=list, description="List of available tools" 32 | ) 33 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_client/transport.py: -------------------------------------------------------------------------------- 1 | """Transport layer for MCP client connections.""" 2 | 3 | import os 4 | from contextlib import AsyncExitStack 5 | from typing import Any 6 | 7 | from mcp import ClientSession, StdioServerParameters 8 | from mcp.client.stdio import stdio_client 9 | 10 | 11 | class MCPTransport: 12 | """Handles MCP server connections via stdio transport.""" 13 | 14 | def __init__(self, server_path: str): 15 | """Initialize transport with server path. 16 | 17 | Args: 18 | server_path: Path to the MCP server script 19 | """ 20 | self.server_path = server_path 21 | self.session: ClientSession | None = None 22 | self.exit_stack: AsyncExitStack | None = None 23 | self.connected = False 24 | self.available_tools: list[str] = [] 25 | 26 | async def connect(self) -> None: 27 | """Connect to MCP server via stdio transport. 28 | 29 | Raises: 30 | FileNotFoundError: If server script doesn't exist 31 | ConnectionError: If connection fails 32 | ValueError: If server script type is not supported 33 | """ 34 | # Validate server script exists 35 | if not os.path.exists(self.server_path): 36 | raise FileNotFoundError(f"Server script not found: {self.server_path}") 37 | 38 | # Determine server type and command 39 | if self.server_path.endswith(".py"): 40 | # Use uv run python for proper environment 41 | command = "uv" 42 | args = ["run", "python", self.server_path] 43 | elif self.server_path.endswith(".js"): 44 | command = "node" 45 | args = [self.server_path] 46 | else: 47 | raise ValueError(f"Unsupported server script type: {self.server_path}") 48 | 49 | # Create server parameters 50 | server_params = StdioServerParameters(command=command, args=args, env=None) 51 | 52 | try: 53 | # Setup resource management 54 | self.exit_stack = AsyncExitStack() 55 | 56 | # Connect to server 57 | read, write = await self.exit_stack.enter_async_context( 58 | stdio_client(server_params) 59 | ) 60 | 61 | # Create session 62 | self.session = await self.exit_stack.enter_async_context( 63 | ClientSession(read, write) 64 | ) 65 | 66 | # Initialize connection 67 | await self.session.initialize() 68 | 69 | # Discover available tools 70 | tools_response = await self.session.list_tools() 71 | self.available_tools = [tool.name for tool in tools_response.tools] 72 | 73 | self.connected = True 74 | 75 | except Exception as e: 76 | # Clean up on connection failure 77 | if self.exit_stack: 78 | await self.exit_stack.aclose() 79 | self.exit_stack = None 80 | raise ConnectionError(f"Failed to connect to server: {e}") 81 | 82 | async def disconnect(self) -> None: 83 | """Disconnect from MCP server.""" 84 | if self.exit_stack: 85 | await self.exit_stack.aclose() 86 | self.exit_stack = None 87 | 88 | self.session = None 89 | self.connected = False 90 | self.available_tools = [] 91 | 92 | async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: 93 | """Call a tool on the connected server. 94 | 95 | Args: 96 | tool_name: Name of the tool to call 97 | arguments: Arguments to pass to the tool 98 | 99 | Returns: 100 | Tool response content 101 | 102 | Raises: 103 | RuntimeError: If not connected to server 104 | ValueError: If tool is not available 105 | """ 106 | if not self.connected or not self.session: 107 | raise RuntimeError("Not connected to server") 108 | 109 | if tool_name not in self.available_tools: 110 | raise ValueError( 111 | f"Tool '{tool_name}' not available. " 112 | f"Available tools: {self.available_tools}" 113 | ) 114 | 115 | # Call the tool 116 | result = await self.session.call_tool(tool_name, arguments) 117 | return result 118 | 119 | async def health_check(self) -> bool: 120 | """Check if connection is healthy. 121 | 122 | Returns: 123 | True if connection is healthy, False otherwise 124 | """ 125 | if not self.connected or not self.session: 126 | return False 127 | 128 | try: 129 | # Try to list tools as a health check 130 | await self.session.list_tools() 131 | return True 132 | except Exception: 133 | return False 134 | 135 | async def __aenter__(self) -> "MCPTransport": 136 | """Async context manager entry.""" 137 | await self.connect() 138 | return self 139 | 140 | async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 141 | """Async context manager exit.""" 142 | await self.disconnect() 143 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP server package.""" 2 | 3 | from .server import mcp, run_server 4 | 5 | __all__ = ["mcp", "run_server"] 6 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/models/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP server data models.""" 2 | 3 | from .requests import ( 4 | DateTimeRequest, 5 | DateTimeResponse, 6 | DiceRollRequest, 7 | DiceRollResponse, 8 | MCPError, 9 | MCPRequest, 10 | MCPResponse, 11 | ToolCallRequest, 12 | ToolCallResponse, 13 | WeatherRequest, 14 | WeatherResponse, 15 | ) 16 | 17 | __all__ = [ 18 | "DateTimeRequest", 19 | "DateTimeResponse", 20 | "DiceRollRequest", 21 | "DiceRollResponse", 22 | "MCPError", 23 | "MCPRequest", 24 | "MCPResponse", 25 | "ToolCallRequest", 26 | "ToolCallResponse", 27 | "WeatherRequest", 28 | "WeatherResponse", 29 | ] 30 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/models/requests.py: -------------------------------------------------------------------------------- 1 | """Pydantic models for MCP server request/response validation.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from pydantic import BaseModel, Field, field_validator 7 | 8 | 9 | class MCPRequest(BaseModel): 10 | """Base MCP request structure following JSON-RPC 2.0 format.""" 11 | 12 | jsonrpc: str = "2.0" 13 | method: str 14 | params: dict | None = None 15 | id: str | int | None = None 16 | 17 | 18 | class MCPResponse(BaseModel): 19 | """Base MCP response structure following JSON-RPC 2.0 format.""" 20 | 21 | jsonrpc: str = "2.0" 22 | id: str | int | None = None 23 | result: Any | None = None 24 | error: dict | None = None 25 | 26 | 27 | class MCPError(BaseModel): 28 | """MCP error structure.""" 29 | 30 | code: int 31 | message: str 32 | data: dict | None = None 33 | 34 | 35 | class DiceRollRequest(BaseModel): 36 | """Dice roll tool request with notation validation.""" 37 | 38 | notation: str = Field(..., description="Dice notation like '2d6' or '1d20'") 39 | 40 | @field_validator("notation") 41 | @classmethod 42 | def validate_notation(cls, v: str) -> str: 43 | """Validate dice notation format.""" 44 | if not isinstance(v, str): 45 | raise ValueError("Notation must be a string") 46 | 47 | # Remove spaces and convert to lowercase 48 | notation = v.strip().lower() 49 | 50 | # Validate format using regex 51 | pattern = re.compile(r"^(\d+)d(\d+)$") 52 | match = pattern.match(notation) 53 | 54 | if not match: 55 | raise ValueError( 56 | f"Invalid dice notation: '{v}'. " 57 | f"Expected format: 'XdY' (e.g., '2d6', '1d20')" 58 | ) 59 | 60 | dice_count = int(match.group(1)) 61 | sides = int(match.group(2)) 62 | 63 | # Validate reasonable limits 64 | if dice_count <= 0: 65 | raise ValueError("Dice count must be greater than 0") 66 | if dice_count > 100: 67 | raise ValueError("Dice count must not exceed 100") 68 | 69 | if sides <= 0: 70 | raise ValueError("Number of sides must be greater than 0") 71 | if sides > 1000: 72 | raise ValueError("Number of sides must not exceed 1000") 73 | 74 | return notation 75 | 76 | 77 | class DiceRollResponse(BaseModel): 78 | """Dice roll tool response.""" 79 | 80 | values: list[int] = Field(..., description="Individual dice roll results") 81 | total: int = Field(..., description="Sum of all dice rolls") 82 | notation: str = Field(..., description="Original dice notation") 83 | 84 | 85 | class WeatherRequest(BaseModel): 86 | """Weather tool request with location validation.""" 87 | 88 | location: str = Field(..., description="City name or coordinates (lat,lon)") 89 | 90 | @field_validator("location") 91 | @classmethod 92 | def validate_location(cls, v: str) -> str: 93 | """Validate location format.""" 94 | if not isinstance(v, str): 95 | raise ValueError("Location must be a string") 96 | 97 | location = v.strip() 98 | if not location: 99 | raise ValueError("Location cannot be empty") 100 | 101 | return location 102 | 103 | 104 | class WeatherResponse(BaseModel): 105 | """Weather tool response.""" 106 | 107 | location: str = Field(..., description="Requested location") 108 | temperature: float = Field(..., description="Temperature in Celsius") 109 | condition: str = Field(..., description="Weather condition description") 110 | wind_speed: float = Field(..., description="Wind speed in km/h") 111 | humidity: float | None = Field(None, description="Humidity percentage") 112 | timestamp: str | None = Field(None, description="Data timestamp") 113 | 114 | 115 | class DateTimeRequest(BaseModel): 116 | """Date/time tool request with timezone validation.""" 117 | 118 | timezone: str = Field( 119 | "UTC", description="Timezone identifier (e.g., 'UTC', 'America/New_York')" 120 | ) 121 | 122 | @field_validator("timezone") 123 | @classmethod 124 | def validate_timezone(cls, v: str) -> str: 125 | """Validate timezone format.""" 126 | if not isinstance(v, str): 127 | raise ValueError("Timezone must be a string") 128 | 129 | timezone = v.strip() 130 | if not timezone: 131 | raise ValueError("Timezone cannot be empty") 132 | 133 | return timezone 134 | 135 | 136 | class DateTimeResponse(BaseModel): 137 | """Date/time tool response.""" 138 | 139 | datetime: str = Field(..., description="ISO 8601 formatted date/time") 140 | timezone: str = Field(..., description="Timezone identifier") 141 | timestamp: float = Field(..., description="Unix timestamp") 142 | 143 | 144 | class ToolCallRequest(BaseModel): 145 | """Generic tool call request.""" 146 | 147 | name: str = Field(..., description="Tool name") 148 | arguments: dict = Field(..., description="Tool arguments") 149 | 150 | 151 | class ToolCallResponse(BaseModel): 152 | """Generic tool call response.""" 153 | 154 | content: list[dict] = Field(..., description="Tool response content") 155 | isError: bool = Field(False, description="Whether this is an error response") 156 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/server.py: -------------------------------------------------------------------------------- 1 | """MCP server implementation with dice, weather, and date/time tools.""" 2 | 3 | import logging 4 | import sys 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from mcp.server.fastmcp import FastMCP 9 | 10 | from src.mcp_server.tools.date_time import DateTimeTool 11 | from src.mcp_server.tools.dice import DiceRollTool 12 | from src.mcp_server.tools.weather import WeatherTool 13 | 14 | # Add project root to Python path 15 | project_root = Path(__file__).parent.parent.parent 16 | sys.path.insert(0, str(project_root)) 17 | 18 | # Configure logging 19 | logging.basicConfig(level=logging.INFO) 20 | logger = logging.getLogger(__name__) 21 | 22 | # Create MCP server instance 23 | mcp = FastMCP("dice-weather-datetime-server") 24 | 25 | # Initialize tool instances 26 | dice_tool = DiceRollTool() 27 | weather_tool = WeatherTool() 28 | datetime_tool = DateTimeTool() 29 | 30 | 31 | @mcp.tool() 32 | async def roll_dice(notation: str) -> dict[str, Any]: 33 | """Roll dice using standard notation like '2d6' or '1d20'. 34 | 35 | Args: 36 | notation: Dice notation (e.g., "2d6", "1d20", "3d10") 37 | 38 | Returns: 39 | Dict containing dice roll results, total, and formatted display 40 | """ 41 | logger.info(f"Tool call: roll_dice(notation='{notation}')") 42 | return await dice_tool.safe_execute(notation=notation) 43 | 44 | 45 | @mcp.tool() 46 | async def get_weather(location: str) -> dict[str, Any]: 47 | """Get current weather conditions for a location. 48 | 49 | Args: 50 | location: City name or coordinates (lat,lon) 51 | 52 | Returns: 53 | Dict containing weather data including temperature, condition, and wind speed 54 | """ 55 | logger.info(f"Tool call: get_weather(location='{location}')") 56 | return await weather_tool.safe_execute(location=location) 57 | 58 | 59 | @mcp.tool() 60 | async def get_date(timezone: str = "UTC") -> dict[str, Any]: 61 | """Get current date and time for a specific timezone. 62 | 63 | Args: 64 | timezone: Timezone identifier (e.g., "UTC", "America/New_York") or alias 65 | 66 | Returns: 67 | Dict containing current date/time in ISO 8601 format with timezone info 68 | """ 69 | logger.info(f"Tool call: get_date(timezone='{timezone}')") 70 | return await datetime_tool.safe_execute(timezone=timezone) 71 | 72 | 73 | @mcp.resource("mcp://tools/help") 74 | async def get_help() -> str: 75 | """Get help information about available tools.""" 76 | help_text = """ 77 | 🎲 **MCP Server - Available Tools** 78 | 79 | **roll_dice** - Roll dice using standard notation 80 | - Usage: roll_dice(notation="2d6") 81 | - Examples: "1d20", "3d6", "2d10" 82 | - Returns individual values and total 83 | 84 | **get_weather** - Get current weather conditions 85 | - Usage: get_weather(location="San Francisco") 86 | - Supports city names or coordinates (lat,lon) 87 | - Returns temperature, condition, wind speed 88 | 89 | **get_date** - Get current date and time 90 | - Usage: get_date(timezone="UTC") 91 | - Supports IANA timezones and common aliases 92 | - Returns ISO 8601 formatted datetime 93 | 94 | **Examples:** 95 | - roll_dice("2d6") → Roll two six-sided dice 96 | - get_weather("London") → Weather for London 97 | - get_date("America/New_York") → NYC current time 98 | """ 99 | return help_text 100 | 101 | 102 | async def cleanup_server(): 103 | """Cleanup server resources.""" 104 | logger.info("Cleaning up server resources...") 105 | try: 106 | await weather_tool.cleanup() 107 | logger.info("Server cleanup completed") 108 | except Exception as e: 109 | logger.error(f"Error during cleanup: {e}") 110 | 111 | 112 | # Server lifecycle management 113 | async def startup(): 114 | """Server startup handler.""" 115 | logger.info("MCP Server starting up...") 116 | logger.info("Tools available: roll_dice, get_weather, get_date") 117 | 118 | 119 | async def shutdown(): 120 | """Server shutdown handler.""" 121 | logger.info("MCP Server shutting down...") 122 | await cleanup_server() 123 | 124 | 125 | def run_server(): 126 | """Run the MCP server.""" 127 | try: 128 | logger.info("Starting MCP server...") 129 | mcp.run() 130 | except KeyboardInterrupt: 131 | logger.info("Server interrupted by user") 132 | except Exception as e: 133 | logger.error(f"Server error: {e}") 134 | raise 135 | 136 | 137 | if __name__ == "__main__": 138 | run_server() 139 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP server tools package.""" 2 | 3 | from .base import ( 4 | AsyncHttpMixin, 5 | BaseTool, 6 | ExternalServiceError, 7 | ToolError, 8 | ValidationToolError, 9 | ) 10 | 11 | __all__ = [ 12 | "AsyncHttpMixin", 13 | "BaseTool", 14 | "ExternalServiceError", 15 | "ToolError", 16 | "ValidationToolError", 17 | ] 18 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/tools/base.py: -------------------------------------------------------------------------------- 1 | """Base tool interface and common patterns for MCP server tools.""" 2 | 3 | import logging 4 | from abc import ABC, abstractmethod 5 | from typing import Any 6 | 7 | import httpx 8 | from pydantic import BaseModel, ValidationError 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class ToolError(Exception): 14 | """Base exception for tool-related errors.""" 15 | 16 | def __init__( 17 | self, message: str, code: int = -1, data: dict[str, Any] | None = None 18 | ): 19 | super().__init__(message) 20 | self.message = message 21 | self.code = code 22 | self.data = data or {} 23 | 24 | 25 | class ValidationToolError(ToolError): 26 | """Exception for input validation errors.""" 27 | 28 | def __init__(self, message: str, validation_errors: list | None = None): 29 | super().__init__(message, code=-32602) # Invalid params error code 30 | self.validation_errors = validation_errors or [] 31 | 32 | 33 | class ExternalServiceError(ToolError): 34 | """Exception for external service errors.""" 35 | 36 | def __init__(self, message: str, service_name: str, status_code: int | None = None): 37 | super().__init__(message, code=-32603) # Internal error code 38 | self.service_name = service_name 39 | self.status_code = status_code 40 | 41 | 42 | class BaseTool(ABC): 43 | """Abstract base class for all MCP server tools.""" 44 | 45 | def __init__(self, name: str, description: str): 46 | self.name = name 47 | self.description = description 48 | self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") 49 | 50 | @abstractmethod 51 | async def execute(self, **kwargs: Any) -> Any: 52 | """Execute the tool with the given arguments.""" 53 | pass 54 | 55 | def validate_input( 56 | self, input_data: dict[str, Any], model_class: type[BaseModel] 57 | ) -> BaseModel: 58 | """Validate input data against a Pydantic model.""" 59 | try: 60 | return model_class(**input_data) 61 | except ValidationError as e: 62 | error_messages = [] 63 | for error in e.errors(): 64 | field = " -> ".join(str(x) for x in error["loc"]) 65 | message = error["msg"] 66 | error_messages.append(f"{field}: {message}") 67 | 68 | raise ValidationToolError( 69 | f"Invalid input for {self.name}: {'; '.join(error_messages)}", 70 | validation_errors=e.errors(), 71 | ) 72 | 73 | def create_success_response(self, data: Any) -> dict[str, Any]: 74 | """Create a successful tool response.""" 75 | return { 76 | "content": [ 77 | { 78 | "type": "text", 79 | "text": str(data) if not isinstance(data, dict | list) else data, 80 | } 81 | ], 82 | "isError": False, 83 | } 84 | 85 | def create_error_response(self, error: Exception) -> dict[str, Any]: 86 | """Create an error tool response.""" 87 | if isinstance(error, ToolError): 88 | error_message = error.message 89 | else: 90 | error_message = f"An unexpected error occurred: {str(error)}" 91 | 92 | # Log the full error for debugging 93 | self.logger.error(f"Tool {self.name} error: {error}", exc_info=True) 94 | 95 | return { 96 | "content": [ 97 | { 98 | "type": "text", 99 | "text": error_message, 100 | } 101 | ], 102 | "isError": True, 103 | } 104 | 105 | async def safe_execute(self, **kwargs) -> dict[str, Any]: 106 | """Execute the tool with error handling.""" 107 | try: 108 | result = await self.execute(**kwargs) 109 | return self.create_success_response(result) 110 | except Exception as e: 111 | return self.create_error_response(e) 112 | 113 | 114 | class AsyncHttpMixin: 115 | """Mixin for tools that need HTTP client capabilities.""" 116 | 117 | def __init__(self, *args, **kwargs): 118 | super().__init__(*args, **kwargs) 119 | self._http_client = None 120 | 121 | @property 122 | def http_client(self): 123 | """Get or create HTTP client.""" 124 | if self._http_client is None: 125 | import httpx 126 | 127 | self._http_client = httpx.AsyncClient( 128 | timeout=30.0, 129 | headers={"User-Agent": "MCP-Server/1.0"}, 130 | ) 131 | return self._http_client 132 | 133 | async def cleanup(self): 134 | """Cleanup HTTP client resources.""" 135 | if self._http_client: 136 | await self._http_client.aclose() 137 | self._http_client = None 138 | 139 | async def make_request( 140 | self, method: str, url: str, timeout: float = 10.0, **kwargs 141 | ) -> dict[str, Any]: 142 | """Make an HTTP request with error handling.""" 143 | try: 144 | response = await self.http_client.request( 145 | method=method, url=url, timeout=timeout, **kwargs 146 | ) 147 | response.raise_for_status() 148 | return response.json() 149 | except httpx.TimeoutException: 150 | raise ExternalServiceError( 151 | f"Request to {url} timed out after {timeout} seconds", 152 | service_name="HTTP", 153 | ) 154 | except httpx.HTTPStatusError as e: 155 | raise ExternalServiceError( 156 | f"HTTP error {e.response.status_code}: {e.response.text}", 157 | service_name="HTTP", 158 | status_code=e.response.status_code, 159 | ) 160 | except Exception as e: 161 | raise ExternalServiceError( 162 | f"Failed to make request to {url}: {str(e)}", 163 | service_name="HTTP", 164 | ) 165 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/tools/date_time.py: -------------------------------------------------------------------------------- 1 | """Date and time tool for MCP server with timezone support.""" 2 | 3 | import zoneinfo 4 | from datetime import UTC, datetime 5 | from typing import Any 6 | 7 | from ..models import DateTimeRequest, DateTimeResponse 8 | from .base import BaseTool, ToolError 9 | 10 | 11 | class DateTimeTool(BaseTool): 12 | """Tool for getting current date and time in various timezones.""" 13 | 14 | def __init__(self): 15 | super().__init__( 16 | name="get_date", 17 | description="Get current date and time in ISO 8601 format for any timezone", 18 | ) 19 | 20 | # Common timezone aliases for user convenience 21 | self.timezone_aliases = { 22 | "utc": "UTC", 23 | "gmt": "UTC", 24 | "est": "America/New_York", 25 | "pst": "America/Los_Angeles", 26 | "cst": "America/Chicago", 27 | "mst": "America/Denver", 28 | "edt": "America/New_York", 29 | "pdt": "America/Los_Angeles", 30 | "cdt": "America/Chicago", 31 | "mdt": "America/Denver", 32 | "bst": "Europe/London", 33 | "cet": "Europe/Paris", 34 | "jst": "Asia/Tokyo", 35 | "aest": "Australia/Sydney", 36 | } 37 | 38 | def parse_timezone(self, timezone_str: str) -> zoneinfo.ZoneInfo | type[UTC]: 39 | """Parse timezone string to ZoneInfo object.""" 40 | # Normalize timezone string 41 | tz_lower = timezone_str.lower().strip() 42 | 43 | # Handle UTC as a special case 44 | if tz_lower in ("utc", "gmt"): 45 | return UTC 46 | 47 | # Check aliases 48 | if tz_lower in self.timezone_aliases: 49 | timezone_str = self.timezone_aliases[tz_lower] 50 | 51 | # Try to create ZoneInfo object 52 | try: 53 | if timezone_str.upper() == "UTC": 54 | return UTC 55 | else: 56 | return zoneinfo.ZoneInfo(timezone_str) 57 | except zoneinfo.ZoneInfoNotFoundError: 58 | # Provide helpful error message with suggestions 59 | common_timezones = [ 60 | "UTC", 61 | "America/New_York", 62 | "America/Los_Angeles", 63 | "America/Chicago", 64 | "Europe/London", 65 | "Europe/Paris", 66 | "Asia/Tokyo", 67 | "Australia/Sydney", 68 | ] 69 | suggestions = ", ".join(common_timezones) 70 | aliases = ", ".join(self.timezone_aliases.keys()) 71 | 72 | raise ToolError( 73 | f"Invalid timezone: '{timezone_str}'. " 74 | f"Common timezones: {suggestions}. " 75 | f"Aliases: {aliases}. " 76 | f"Use IANA timezone names (e.g., 'America/New_York') or aliases." 77 | ) 78 | 79 | async def execute(self, **kwargs: Any) -> DateTimeResponse: 80 | """Get current date and time for the specified timezone.""" 81 | timezone = kwargs.get("timezone", "UTC") 82 | 83 | # Validate input 84 | request = self.validate_input({"timezone": timezone}, DateTimeRequest) 85 | 86 | # Parse timezone 87 | tz = self.parse_timezone(request.timezone) 88 | 89 | self.logger.info(f"Getting current time for timezone: {timezone}") 90 | 91 | # Get current time in the specified timezone 92 | if isinstance(tz, zoneinfo.ZoneInfo): 93 | current_time = datetime.now(tz) 94 | tz_name = str(tz) 95 | else: # UTC 96 | current_time = datetime.now(tz) 97 | tz_name = "UTC" 98 | 99 | # Format as ISO 8601 100 | iso_datetime = current_time.isoformat() 101 | 102 | # Get Unix timestamp 103 | timestamp = current_time.timestamp() 104 | 105 | self.logger.info(f"Current time in {tz_name}: {iso_datetime}") 106 | 107 | return DateTimeResponse( 108 | datetime=iso_datetime, 109 | timezone=tz_name, 110 | timestamp=timestamp, 111 | ) 112 | 113 | def format_result(self, response: DateTimeResponse) -> str: 114 | """Format date/time data for display.""" 115 | # Parse the ISO datetime to extract components 116 | try: 117 | dt = datetime.fromisoformat(response.datetime) 118 | date_part = dt.strftime("%Y-%m-%d") 119 | time_part = dt.strftime("%H:%M:%S") 120 | weekday = dt.strftime("%A") 121 | 122 | result = "🕐 **Current Date & Time**\n" 123 | result += f"📅 Date: **{date_part}** ({weekday})\n" 124 | result += f"⏰ Time: **{time_part}**\n" 125 | result += f"🌍 Timezone: **{response.timezone}**\n" 126 | result += f"📋 ISO 8601: `{response.datetime}`\n" 127 | result += f"🔢 Unix Timestamp: `{int(response.timestamp)}`" 128 | 129 | return result 130 | 131 | except ValueError: 132 | # Fallback if datetime parsing fails 133 | return ( 134 | f"🕐 **Current Date & Time**\n" 135 | f"📋 ISO 8601: `{response.datetime}`\n" 136 | f"🌍 Timezone: **{response.timezone}**\n" 137 | f"🔢 Unix Timestamp: `{int(response.timestamp)}`" 138 | ) 139 | 140 | async def safe_execute(self, **kwargs) -> dict[str, Any]: 141 | """Execute date/time lookup with formatted output.""" 142 | try: 143 | result = await self.execute(**kwargs) 144 | formatted_result = self.format_result(result) 145 | 146 | return { 147 | "content": [ 148 | { 149 | "type": "text", 150 | "text": formatted_result, 151 | } 152 | ], 153 | "isError": False, 154 | } 155 | except Exception as e: 156 | return self.create_error_response(e) 157 | 158 | def get_available_timezones(self) -> list[str]: 159 | """Get a list of common available timezones.""" 160 | common_zones = [ 161 | "UTC", 162 | "America/New_York", 163 | "America/Los_Angeles", 164 | "America/Chicago", 165 | "America/Denver", 166 | "America/Phoenix", 167 | "America/Anchorage", 168 | "America/Honolulu", 169 | "America/Toronto", 170 | "America/Vancouver", 171 | "Europe/London", 172 | "Europe/Paris", 173 | "Europe/Berlin", 174 | "Europe/Rome", 175 | "Europe/Madrid", 176 | "Europe/Amsterdam", 177 | "Europe/Stockholm", 178 | "Europe/Moscow", 179 | "Asia/Tokyo", 180 | "Asia/Shanghai", 181 | "Asia/Kolkata", 182 | "Asia/Dubai", 183 | "Asia/Singapore", 184 | "Australia/Sydney", 185 | "Australia/Melbourne", 186 | "Pacific/Auckland", 187 | ] 188 | return sorted(common_zones + list(self.timezone_aliases.values())) 189 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/tools/dice.py: -------------------------------------------------------------------------------- 1 | """Dice rolling tool for MCP server.""" 2 | 3 | import random 4 | import re 5 | from typing import Any 6 | 7 | from ..models import DiceRollRequest, DiceRollResponse 8 | from .base import BaseTool, ToolError 9 | 10 | 11 | class DiceRollTool(BaseTool): 12 | """Tool for rolling dice using standard notation.""" 13 | 14 | def __init__(self): 15 | super().__init__( 16 | name="roll_dice", 17 | description="Roll dice using standard notation like '2d6' or '1d20'", 18 | ) 19 | self.notation_pattern = re.compile(r"^(\d+)d(\d+)$") 20 | 21 | async def execute(self, **kwargs: Any) -> DiceRollResponse: 22 | """Execute dice roll with the given notation.""" 23 | notation = kwargs.get("notation") 24 | if not notation: 25 | raise ToolError("Missing required parameter: notation") 26 | 27 | # Validate input using Pydantic model 28 | request = self.validate_input({"notation": notation}, DiceRollRequest) 29 | 30 | # Parse the notation 31 | match = self.notation_pattern.match(request.notation) 32 | if not match: 33 | raise ToolError(f"Invalid dice notation: {notation}") 34 | 35 | dice_count = int(match.group(1)) 36 | sides = int(match.group(2)) 37 | 38 | self.logger.info(f"Rolling {dice_count}d{sides}") 39 | 40 | # Generate random values for each die 41 | values = [] 42 | for _ in range(dice_count): 43 | roll = random.randint(1, sides) 44 | values.append(roll) 45 | 46 | total = sum(values) 47 | 48 | self.logger.info(f"Dice roll result: {values} (total: {total})") 49 | 50 | # Return structured response 51 | return DiceRollResponse( 52 | values=values, 53 | total=total, 54 | notation=str(notation), # Return original notation as provided 55 | ) 56 | 57 | def format_result(self, response: DiceRollResponse) -> str: 58 | """Format dice roll result for display.""" 59 | if len(response.values) == 1: 60 | return f"🎲 Rolled {response.notation}: **{response.values[0]}**" 61 | else: 62 | values_str = ", ".join(map(str, response.values)) 63 | return ( 64 | f"🎲 Rolled {response.notation}: [{values_str}] = **{response.total}**" 65 | ) 66 | 67 | async def safe_execute(self, **kwargs) -> dict[str, Any]: 68 | """Execute dice roll with formatted output.""" 69 | try: 70 | result = await self.execute(**kwargs) 71 | formatted_result = self.format_result(result) 72 | 73 | return { 74 | "content": [ 75 | { 76 | "type": "text", 77 | "text": formatted_result, 78 | } 79 | ], 80 | "isError": False, 81 | } 82 | except Exception as e: 83 | return self.create_error_response(e) 84 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/mcp_server/tools/weather.py: -------------------------------------------------------------------------------- 1 | """Weather tool for MCP server using Open-Meteo API.""" 2 | 3 | from datetime import datetime 4 | from typing import Any 5 | 6 | from ..models import WeatherRequest, WeatherResponse 7 | from .base import AsyncHttpMixin, BaseTool, ExternalServiceError, ToolError 8 | 9 | 10 | class WeatherTool(BaseTool, AsyncHttpMixin): 11 | """Tool for getting current weather data.""" 12 | 13 | def __init__(self): 14 | super().__init__( 15 | name="get_weather", 16 | description="Get current weather conditions for a location", 17 | ) 18 | self.api_base = "https://api.open-meteo.com/v1" 19 | 20 | # Basic city to coordinates mapping 21 | # In production, this would use a proper geocoding service 22 | self.city_coords = { 23 | "san francisco": (37.7749, -122.4194), 24 | "new york": (40.7128, -74.0060), 25 | "london": (51.5074, -0.1278), 26 | "paris": (48.8566, 2.3522), 27 | "tokyo": (35.6762, 139.6503), 28 | "sydney": (-33.8688, 151.2093), 29 | "los angeles": (34.0522, -118.2437), 30 | "chicago": (41.8781, -87.6298), 31 | "miami": (25.7617, -80.1918), 32 | "seattle": (47.6062, -122.3321), 33 | "vancouver": (49.2827, -123.1207), 34 | "toronto": (43.6532, -79.3832), 35 | "berlin": (52.5200, 13.4050), 36 | "rome": (41.9028, 12.4964), 37 | "madrid": (40.4168, -3.7038), 38 | "moscow": (55.7558, 37.6176), 39 | "beijing": (39.9042, 116.4074), 40 | "mumbai": (19.0760, 72.8777), 41 | "cairo": (30.0444, 31.2357), 42 | "lagos": (6.5244, 3.3792), 43 | } 44 | 45 | # Weather code to description mapping (subset of WMO codes) 46 | self.weather_codes = { 47 | 0: "Clear sky", 48 | 1: "Mainly clear", 49 | 2: "Partly cloudy", 50 | 3: "Overcast", 51 | 45: "Fog", 52 | 48: "Depositing rime fog", 53 | 51: "Light drizzle", 54 | 53: "Moderate drizzle", 55 | 55: "Dense drizzle", 56 | 61: "Slight rain", 57 | 63: "Moderate rain", 58 | 65: "Heavy rain", 59 | 71: "Slight snow", 60 | 73: "Moderate snow", 61 | 75: "Heavy snow", 62 | 77: "Snow grains", 63 | 80: "Slight rain showers", 64 | 81: "Moderate rain showers", 65 | 82: "Violent rain showers", 66 | 85: "Slight snow showers", 67 | 86: "Heavy snow showers", 68 | 95: "Thunderstorm", 69 | 96: "Thunderstorm with slight hail", 70 | 99: "Thunderstorm with heavy hail", 71 | } 72 | 73 | def parse_location(self, location: str) -> tuple[float, float]: 74 | """Parse location string to get coordinates.""" 75 | location_lower = location.lower().strip() 76 | 77 | # Check if it's in our city mapping 78 | if location_lower in self.city_coords: 79 | return self.city_coords[location_lower] 80 | 81 | # Try to parse as "lat,lon" coordinates 82 | try: 83 | parts = location.split(",") 84 | if len(parts) == 2: 85 | lat = float(parts[0].strip()) 86 | lon = float(parts[1].strip()) 87 | 88 | # Basic validation for reasonable coordinate ranges 89 | if -90 <= lat <= 90 and -180 <= lon <= 180: 90 | return lat, lon 91 | except ValueError: 92 | pass 93 | 94 | # If we can't parse the location, raise an error 95 | available_cities = ", ".join(sorted(self.city_coords.keys())) 96 | raise ToolError( 97 | f"Unknown location: '{location}'. " 98 | f"Please use coordinates (lat,lon) or one of: {available_cities}" 99 | ) 100 | 101 | def weather_code_to_text(self, code: int) -> str: 102 | """Convert weather code to readable description.""" 103 | return self.weather_codes.get(code, f"Unknown weather condition (code: {code})") 104 | 105 | async def execute(self, **kwargs: Any) -> WeatherResponse: 106 | """Get weather data for the specified location.""" 107 | location = kwargs.get("location") 108 | if not location: 109 | raise ToolError("Missing required parameter: location") 110 | 111 | # Validate input 112 | request = self.validate_input({"location": location}, WeatherRequest) 113 | 114 | # Parse location to coordinates 115 | lat, lon = self.parse_location(request.location) 116 | 117 | self.logger.info(f"Getting weather for {location} ({lat}, {lon})") 118 | 119 | # Make API request to Open-Meteo 120 | try: 121 | data = await self.make_request( 122 | method="GET", 123 | url=f"{self.api_base}/forecast", 124 | params={ 125 | "latitude": lat, 126 | "longitude": lon, 127 | "current": ( 128 | "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m" 129 | ), 130 | "timezone": "auto", 131 | }, 132 | timeout=15.0, 133 | ) 134 | 135 | # Extract current weather data 136 | current = data.get("current", {}) 137 | if not current: 138 | raise ExternalServiceError( 139 | "No current weather data available", 140 | service_name="Open-Meteo", 141 | ) 142 | 143 | temperature = current.get("temperature_2m") 144 | humidity = current.get("relative_humidity_2m") 145 | weather_code = current.get("weather_code") 146 | wind_speed = current.get("wind_speed_10m") 147 | timestamp = current.get("time") 148 | 149 | if temperature is None or weather_code is None or wind_speed is None: 150 | raise ExternalServiceError( 151 | "Incomplete weather data received", 152 | service_name="Open-Meteo", 153 | ) 154 | 155 | condition = self.weather_code_to_text(weather_code) 156 | 157 | self.logger.info( 158 | f"Weather data retrieved: {temperature}°C, {condition}, " 159 | f"{wind_speed} km/h wind" 160 | ) 161 | 162 | return WeatherResponse( 163 | location=str(location), 164 | temperature=temperature, 165 | condition=condition, 166 | wind_speed=wind_speed, 167 | humidity=humidity, 168 | timestamp=timestamp, 169 | ) 170 | 171 | except ExternalServiceError: 172 | raise 173 | except Exception as e: 174 | raise ExternalServiceError( 175 | f"Failed to retrieve weather data: {str(e)}", 176 | service_name="Open-Meteo", 177 | ) 178 | 179 | def format_result(self, response: WeatherResponse) -> str: 180 | """Format weather data for display.""" 181 | result = f"🌤️ **Weather for {response.location}**\n" 182 | result += f"🌡️ Temperature: **{response.temperature}°C**\n" 183 | result += f"☁️ Condition: **{response.condition}**\n" 184 | result += f"💨 Wind Speed: **{response.wind_speed} km/h**" 185 | 186 | if response.humidity is not None: 187 | result += f"\n💧 Humidity: **{response.humidity}%**" 188 | 189 | if response.timestamp: 190 | try: 191 | # Parse and format timestamp 192 | dt = datetime.fromisoformat(response.timestamp.replace("Z", "+00:00")) 193 | result += f"\n🕐 Updated: {dt.strftime('%Y-%m-%d %H:%M UTC')}" 194 | except ValueError: 195 | pass 196 | 197 | return result 198 | 199 | async def safe_execute(self, **kwargs) -> dict[str, Any]: 200 | """Execute weather lookup with formatted output.""" 201 | try: 202 | result = await self.execute(**kwargs) 203 | formatted_result = self.format_result(result) 204 | 205 | return { 206 | "content": [ 207 | { 208 | "type": "text", 209 | "text": formatted_result, 210 | } 211 | ], 212 | "isError": False, 213 | } 214 | except Exception as e: 215 | return self.create_error_response(e) 216 | -------------------------------------------------------------------------------- /examples/mcp-server-client/src/py.typed: -------------------------------------------------------------------------------- 1 | # PEP 561 – Distributing and Packaging Type Information 2 | # https://peps.python.org/pep-0561/ -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/examples/mcp-server-client/tests/__init__.py -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/examples/mcp-server-client/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/fixtures/mcp_messages.py: -------------------------------------------------------------------------------- 1 | """Test fixtures for MCP messages and responses.""" 2 | 3 | from typing import Any 4 | 5 | 6 | class MCPMessageFixtures: 7 | """Collection of MCP message fixtures for testing.""" 8 | 9 | @staticmethod 10 | def tool_call_request( 11 | tool_name: str, arguments: dict[str, Any], request_id: int = 1 12 | ) -> dict[str, Any]: 13 | """Create a standard MCP tool call request.""" 14 | return { 15 | "jsonrpc": "2.0", 16 | "method": "tools/call", 17 | "params": { 18 | "name": tool_name, 19 | "arguments": arguments, 20 | }, 21 | "id": request_id, 22 | } 23 | 24 | @staticmethod 25 | def success_response(content: Any, request_id: int = 1) -> dict[str, Any]: 26 | """Create a successful MCP response.""" 27 | return { 28 | "jsonrpc": "2.0", 29 | "id": request_id, 30 | "result": { 31 | "content": [ 32 | { 33 | "type": "text", 34 | "text": content, 35 | } 36 | ], 37 | "isError": False, 38 | }, 39 | } 40 | 41 | @staticmethod 42 | def error_response(error_message: str, request_id: int = 1) -> dict[str, Any]: 43 | """Create an error MCP response.""" 44 | return { 45 | "jsonrpc": "2.0", 46 | "id": request_id, 47 | "result": { 48 | "content": [ 49 | { 50 | "type": "text", 51 | "text": error_message, 52 | } 53 | ], 54 | "isError": True, 55 | }, 56 | } 57 | 58 | 59 | class WeatherAPIFixtures: 60 | """Collection of weather API response fixtures.""" 61 | 62 | @staticmethod 63 | def current_weather_response( 64 | temperature: float = 20.0, 65 | weather_code: int = 0, 66 | wind_speed: float = 10.0, 67 | humidity: float = 65.0, 68 | ) -> dict[str, Any]: 69 | """Create a mock Open-Meteo API response.""" 70 | return { 71 | "current": { 72 | "time": "2025-07-07T14:30:00Z", 73 | "temperature_2m": temperature, 74 | "relative_humidity_2m": humidity, 75 | "weather_code": weather_code, 76 | "wind_speed_10m": wind_speed, 77 | }, 78 | "current_units": { 79 | "time": "iso8601", 80 | "temperature_2m": "°C", 81 | "relative_humidity_2m": "%", 82 | "weather_code": "wmo code", 83 | "wind_speed_10m": "km/h", 84 | }, 85 | } 86 | 87 | @staticmethod 88 | def api_error_response(status_code: int = 500) -> dict[str, Any]: 89 | """Create a mock API error response.""" 90 | return { 91 | "error": True, 92 | "reason": "Internal server error" if status_code == 500 else "Bad request", 93 | } 94 | -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/test_mcp_server.py: -------------------------------------------------------------------------------- 1 | """Tests for the MCP server integration.""" 2 | 3 | from unittest.mock import AsyncMock, patch 4 | 5 | import pytest 6 | 7 | from src.mcp_server.server import cleanup_server, datetime_tool, dice_tool, weather_tool 8 | from tests.fixtures.mcp_messages import WeatherAPIFixtures 9 | 10 | 11 | class TestMCPServerIntegration: 12 | """Test suite for MCP server integration.""" 13 | 14 | @pytest.mark.asyncio 15 | async def test_dice_tool_integration(self): 16 | """Test dice tool integration through MCP server.""" 17 | # Import the MCP tool function 18 | from src.mcp_server.server import roll_dice 19 | 20 | result = await roll_dice(notation="2d6") 21 | 22 | assert "content" in result 23 | assert "isError" in result 24 | assert result["isError"] is False 25 | assert "🎲" in result["content"][0]["text"] 26 | assert "2d6" in result["content"][0]["text"] 27 | 28 | @pytest.mark.asyncio 29 | async def test_weather_tool_integration(self): 30 | """Test weather tool integration through MCP server.""" 31 | from src.mcp_server.server import get_weather 32 | 33 | mock_response = WeatherAPIFixtures.current_weather_response() 34 | 35 | with patch.object(weather_tool, "make_request", return_value=mock_response): 36 | result = await get_weather(location="San Francisco") 37 | 38 | assert "content" in result 39 | assert "isError" in result 40 | assert result["isError"] is False 41 | assert "🌤️" in result["content"][0]["text"] 42 | assert "San Francisco" in result["content"][0]["text"] 43 | 44 | @pytest.mark.asyncio 45 | async def test_datetime_tool_integration(self): 46 | """Test date/time tool integration through MCP server.""" 47 | from src.mcp_server.server import get_date 48 | 49 | result = await get_date(timezone="UTC") 50 | 51 | assert "content" in result 52 | assert "isError" in result 53 | assert result["isError"] is False 54 | assert "🕐" in result["content"][0]["text"] 55 | assert "UTC" in result["content"][0]["text"] 56 | 57 | @pytest.mark.asyncio 58 | async def test_dice_tool_error_handling(self): 59 | """Test error handling in dice tool integration.""" 60 | from src.mcp_server.server import roll_dice 61 | 62 | result = await roll_dice(notation="invalid") 63 | 64 | assert "content" in result 65 | assert "isError" in result 66 | assert result["isError"] is True 67 | assert "Invalid input" in result["content"][0]["text"] 68 | 69 | @pytest.mark.asyncio 70 | async def test_weather_tool_error_handling(self): 71 | """Test error handling in weather tool integration.""" 72 | from src.mcp_server.server import get_weather 73 | 74 | result = await get_weather(location="Unknown City") 75 | 76 | assert "content" in result 77 | assert "isError" in result 78 | assert result["isError"] is True 79 | assert "Unknown location" in result["content"][0]["text"] 80 | 81 | @pytest.mark.asyncio 82 | async def test_datetime_tool_error_handling(self): 83 | """Test error handling in date/time tool integration.""" 84 | from src.mcp_server.server import get_date 85 | 86 | result = await get_date(timezone="Invalid/Timezone") 87 | 88 | assert "content" in result 89 | assert "isError" in result 90 | assert result["isError"] is True 91 | assert "Invalid timezone" in result["content"][0]["text"] 92 | 93 | @pytest.mark.asyncio 94 | async def test_help_resource(self): 95 | """Test help resource is available.""" 96 | from src.mcp_server.server import get_help 97 | 98 | help_text = await get_help() 99 | 100 | assert isinstance(help_text, str) 101 | assert "roll_dice" in help_text 102 | assert "get_weather" in help_text 103 | assert "get_date" in help_text 104 | assert "🎲" in help_text 105 | 106 | @pytest.mark.asyncio 107 | async def test_cleanup_server(self): 108 | """Test server cleanup functionality.""" 109 | # Mock the weather tool's cleanup method 110 | weather_tool.cleanup = AsyncMock() 111 | 112 | await cleanup_server() 113 | 114 | weather_tool.cleanup.assert_called_once() 115 | 116 | @pytest.mark.asyncio 117 | async def test_cleanup_server_with_error(self): 118 | """Test server cleanup handles errors gracefully.""" 119 | # Mock the weather tool's cleanup method to raise an error 120 | weather_tool.cleanup = AsyncMock(side_effect=Exception("Cleanup error")) 121 | 122 | # Should not raise an exception 123 | await cleanup_server() 124 | 125 | weather_tool.cleanup.assert_called_once() 126 | 127 | def test_tool_instances_exist(self): 128 | """Test that tool instances are properly created.""" 129 | assert dice_tool is not None 130 | assert weather_tool is not None 131 | assert datetime_tool is not None 132 | 133 | assert dice_tool.name == "roll_dice" 134 | assert weather_tool.name == "get_weather" 135 | assert datetime_tool.name == "get_date" 136 | 137 | @pytest.mark.asyncio 138 | async def test_mcp_tool_docstrings(self): 139 | """Test that MCP tool functions have proper docstrings.""" 140 | from src.mcp_server.server import get_date, get_weather, roll_dice 141 | 142 | assert roll_dice.__doc__ is not None 143 | assert "dice" in roll_dice.__doc__.lower() 144 | assert "notation" in roll_dice.__doc__.lower() 145 | 146 | assert get_weather.__doc__ is not None 147 | assert "weather" in get_weather.__doc__.lower() 148 | assert "location" in get_weather.__doc__.lower() 149 | 150 | assert get_date.__doc__ is not None 151 | assert "date" in get_date.__doc__.lower() 152 | assert "timezone" in get_date.__doc__.lower() 153 | 154 | @pytest.mark.asyncio 155 | async def test_all_tools_return_proper_format(self): 156 | """Test that all tools return the expected MCP response format.""" 157 | from src.mcp_server.server import get_date, roll_dice 158 | 159 | # Test dice tool 160 | dice_result = await roll_dice(notation="1d6") 161 | assert "content" in dice_result 162 | assert "isError" in dice_result 163 | assert isinstance(dice_result["content"], list) 164 | assert len(dice_result["content"]) == 1 165 | assert "type" in dice_result["content"][0] 166 | assert "text" in dice_result["content"][0] 167 | 168 | # Test date tool 169 | date_result = await get_date(timezone="UTC") 170 | assert "content" in date_result 171 | assert "isError" in date_result 172 | assert isinstance(date_result["content"], list) 173 | assert len(date_result["content"]) == 1 174 | assert "type" in date_result["content"][0] 175 | assert "text" in date_result["content"][0] 176 | 177 | @pytest.mark.asyncio 178 | async def test_weather_tool_with_coordinates(self): 179 | """Test weather tool with coordinate input.""" 180 | from src.mcp_server.server import get_weather 181 | 182 | mock_response = WeatherAPIFixtures.current_weather_response() 183 | 184 | with patch.object(weather_tool, "make_request", return_value=mock_response): 185 | result = await get_weather(location="37.7749,-122.4194") 186 | 187 | assert "content" in result 188 | assert "isError" in result 189 | assert result["isError"] is False 190 | assert "37.7749,-122.4194" in result["content"][0]["text"] 191 | -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/test_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qte77/context-engineering-template/e35a269c47a5c9a91c90609d25c9b1a611f1858f/examples/mcp-server-client/tests/test_tools/__init__.py -------------------------------------------------------------------------------- /examples/mcp-server-client/tests/test_tools/test_dice.py: -------------------------------------------------------------------------------- 1 | """Tests for the dice rolling tool.""" 2 | 3 | import pytest 4 | 5 | from src.mcp_server.tools.base import ValidationToolError 6 | from src.mcp_server.tools.dice import DiceRollTool 7 | 8 | 9 | class TestDiceRollTool: 10 | """Test suite for DiceRollTool.""" 11 | 12 | @pytest.fixture 13 | def dice_tool(self): 14 | """Create a DiceRollTool instance for testing.""" 15 | return DiceRollTool() 16 | 17 | @pytest.mark.asyncio 18 | async def test_roll_dice_valid_notation(self, dice_tool): 19 | """Test valid dice notation works correctly.""" 20 | result = await dice_tool.execute(notation="2d6") 21 | 22 | assert isinstance(result.values, list) 23 | assert len(result.values) == 2 24 | assert all(1 <= v <= 6 for v in result.values) 25 | assert result.total == sum(result.values) 26 | assert result.notation == "2d6" 27 | 28 | @pytest.mark.asyncio 29 | async def test_roll_dice_single_die(self, dice_tool): 30 | """Test rolling a single die.""" 31 | result = await dice_tool.execute(notation="1d20") 32 | 33 | assert len(result.values) == 1 34 | assert 1 <= result.values[0] <= 20 35 | assert result.total == result.values[0] 36 | assert result.notation == "1d20" 37 | 38 | @pytest.mark.asyncio 39 | async def test_roll_dice_multiple_dice(self, dice_tool): 40 | """Test rolling multiple dice.""" 41 | result = await dice_tool.execute(notation="4d10") 42 | 43 | assert len(result.values) == 4 44 | assert all(1 <= v <= 10 for v in result.values) 45 | assert result.total == sum(result.values) 46 | assert result.notation == "4d10" 47 | 48 | @pytest.mark.asyncio 49 | async def test_roll_dice_invalid_notation_format(self, dice_tool): 50 | """Test invalid dice notation raises ValidationError.""" 51 | with pytest.raises(ValidationToolError) as exc_info: 52 | await dice_tool.execute(notation="d6") 53 | 54 | assert "Invalid dice notation" in str(exc_info.value) 55 | 56 | @pytest.mark.asyncio 57 | async def test_roll_dice_invalid_notation_no_d(self, dice_tool): 58 | """Test notation without 'd' raises ValidationError.""" 59 | with pytest.raises(ValidationToolError) as exc_info: 60 | await dice_tool.execute(notation="2x6") 61 | 62 | assert "Invalid dice notation" in str(exc_info.value) 63 | 64 | @pytest.mark.asyncio 65 | async def test_roll_dice_zero_dice_count(self, dice_tool): 66 | """Test zero dice count raises ValidationError.""" 67 | with pytest.raises(ValidationToolError) as exc_info: 68 | await dice_tool.execute(notation="0d6") 69 | 70 | assert "Dice count must be greater than 0" in str(exc_info.value) 71 | 72 | @pytest.mark.asyncio 73 | async def test_roll_dice_zero_sides(self, dice_tool): 74 | """Test zero sides raises ValidationError.""" 75 | with pytest.raises(ValidationToolError) as exc_info: 76 | await dice_tool.execute(notation="1d0") 77 | 78 | assert "Number of sides must be greater than 0" in str(exc_info.value) 79 | 80 | @pytest.mark.asyncio 81 | async def test_roll_dice_too_many_dice(self, dice_tool): 82 | """Test too many dice raises ValidationError.""" 83 | with pytest.raises(ValidationToolError) as exc_info: 84 | await dice_tool.execute(notation="101d6") 85 | 86 | assert "Dice count must not exceed 100" in str(exc_info.value) 87 | 88 | @pytest.mark.asyncio 89 | async def test_roll_dice_too_many_sides(self, dice_tool): 90 | """Test too many sides raises ValidationError.""" 91 | with pytest.raises(ValidationToolError) as exc_info: 92 | await dice_tool.execute(notation="1d1001") 93 | 94 | assert "Number of sides must not exceed 1000" in str(exc_info.value) 95 | 96 | @pytest.mark.asyncio 97 | async def test_roll_dice_random_text(self, dice_tool): 98 | """Test random text raises ValidationError.""" 99 | with pytest.raises(ValidationToolError) as exc_info: 100 | await dice_tool.execute(notation="abc") 101 | 102 | assert "Invalid dice notation" in str(exc_info.value) 103 | 104 | def test_format_result_single_die(self, dice_tool): 105 | """Test formatting result for single die.""" 106 | from src.mcp_server.models import DiceRollResponse 107 | 108 | response = DiceRollResponse(values=[15], total=15, notation="1d20") 109 | formatted = dice_tool.format_result(response) 110 | 111 | assert "🎲" in formatted 112 | assert "1d20" in formatted 113 | assert "**15**" in formatted 114 | 115 | def test_format_result_multiple_dice(self, dice_tool): 116 | """Test formatting result for multiple dice.""" 117 | from src.mcp_server.models import DiceRollResponse 118 | 119 | response = DiceRollResponse(values=[4, 2, 6], total=12, notation="3d6") 120 | formatted = dice_tool.format_result(response) 121 | 122 | assert "🎲" in formatted 123 | assert "3d6" in formatted 124 | assert "[4, 2, 6]" in formatted 125 | assert "**12**" in formatted 126 | 127 | @pytest.mark.asyncio 128 | async def test_safe_execute_success(self, dice_tool): 129 | """Test safe_execute returns proper success format.""" 130 | result = await dice_tool.safe_execute(notation="2d6") 131 | 132 | assert "content" in result 133 | assert "isError" in result 134 | assert result["isError"] is False 135 | assert len(result["content"]) == 1 136 | assert result["content"][0]["type"] == "text" 137 | assert "🎲" in result["content"][0]["text"] 138 | 139 | @pytest.mark.asyncio 140 | async def test_safe_execute_error(self, dice_tool): 141 | """Test safe_execute returns proper error format.""" 142 | result = await dice_tool.safe_execute(notation="invalid") 143 | 144 | assert "content" in result 145 | assert "isError" in result 146 | assert result["isError"] is True 147 | assert len(result["content"]) == 1 148 | assert result["content"][0]["type"] == "text" 149 | assert "Invalid input" in result["content"][0]["text"] 150 | 151 | @pytest.mark.asyncio 152 | async def test_roll_dice_case_insensitive(self, dice_tool): 153 | """Test dice notation is case insensitive.""" 154 | result1 = await dice_tool.execute(notation="2D6") 155 | result2 = await dice_tool.execute(notation="2d6") 156 | 157 | # Both should work and produce valid results 158 | assert len(result1.values) == 2 159 | assert len(result2.values) == 2 160 | assert all(1 <= v <= 6 for v in result1.values) 161 | assert all(1 <= v <= 6 for v in result2.values) 162 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.com/james-willett/mkdocs-material-youtube-tutorial 3 | # https://mkdocstrings.github.io/recipes/ 4 | # site info set in workflow 5 | site_name: '' 6 | site_description: '' 7 | repo_url: '' 8 | edit_uri: edit/main 9 | theme: 10 | name: material 11 | language: en 12 | features: 13 | - content.code.annotation 14 | - content.code.copy 15 | - content.tabs.link 16 | - navigation.footer 17 | - navigation.sections 18 | - navigation.tabs 19 | - navigation.top 20 | - toc.integrate 21 | - search.suggest 22 | - search.highlight 23 | palette: 24 | - media: "(prefers-color-scheme: light)" 25 | scheme: default 26 | toggle: 27 | # icon: material/brightness-7 28 | icon: material/toggle-switch-off-outline 29 | name: "Toggle Dark Mode" 30 | - media: "(prefers-color-scheme: dark)" 31 | scheme: slate 32 | toggle: 33 | # icon: material/brightness-4 34 | icon: material/toggle-switch 35 | name: "Toggle Light Mode" 36 | nav: 37 | - Home: index.md 38 | - Code: docstrings.md 39 | - Change Log: CHANGELOG.md 40 | - License: LICENSE 41 | - llms.txt: llms.txt 42 | plugins: 43 | - search: 44 | lang: en 45 | - autorefs 46 | - mkdocstrings: 47 | handlers: 48 | python: 49 | paths: [src] 50 | options: 51 | show_root_heading: true 52 | show_root_full_path: true 53 | show_object_full_path: false 54 | show_root_members_full_path: false 55 | show_category_heading: true 56 | show_submodules: true 57 | markdown_extensions: 58 | - attr_list 59 | - pymdownx.magiclink 60 | - pymdownx.tabbed 61 | - pymdownx.highlight: 62 | anchor_linenums: true 63 | - pymdownx.superfences 64 | - pymdownx.snippets: 65 | check_paths: true 66 | - pymdownx.tasklist: 67 | custom_checkbox: true 68 | - sane_lists 69 | - smarty 70 | - toc: 71 | permalink: true 72 | validation: 73 | links: 74 | not_found: warn 75 | anchors: warn 76 | # builds only if validation succeeds while 77 | # threating warnings as errors 78 | # also checks for broken links 79 | # strict: true 80 | ... 81 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | version = "0.0.2" 7 | name = "context-engineering-template" 8 | description = "Assess the effectiveness of agentic AI systems across various use cases focusing on agnostic metrics that measure core agentic capabilities." 9 | authors = [ 10 | {name = "qte77", email = "qte@77.gh"} 11 | ] 12 | readme = "README.md" 13 | requires-python = "==3.13.*" 14 | license = "bsd-3-clause" 15 | dependencies = [] 16 | 17 | [project.urls] 18 | Documentation = "https://qte77.github.io/context-engineering-template/" 19 | 20 | [dependency-groups] 21 | dev = [] 22 | test = [] 23 | docs = [ 24 | "griffe>=1.5.1", 25 | "mkdocs>=1.6.1", 26 | "mkdocs-awesome-pages-plugin>=2.9.3", 27 | "mkdocs-gen-files>=0.5.0", 28 | "mkdocs-literate-nav>=0.6.1", 29 | "mkdocs-material>=9.5.44", 30 | "mkdocs-section-index>=0.3.8", 31 | "mkdocstrings[python]>=0.27.0", 32 | ] 33 | 34 | [tool.uv] 35 | package = true 36 | exclude-newer = "2025-07-06T00:00:00Z" 37 | 38 | [tool.hatch.build.targets.wheel] 39 | only-include = ["/README.md"] 40 | 41 | [tool.hatch.build.targets.sdist] 42 | include = ["/README.md", "/Makefile", "/tests"] 43 | 44 | [tool.ruff] 45 | target-version = "py313" 46 | src = ["src", "tests"] 47 | 48 | [tool.ruff.format] 49 | docstring-code-format = true 50 | 51 | [tool.ruff.lint] 52 | # ignore = ["E203"] # Whitespace before ':' 53 | unfixable = ["B"] 54 | select = [ 55 | # pycodestyle 56 | "E", 57 | # Pyflakes 58 | "F", 59 | # pyupgrade 60 | "UP", 61 | # isort 62 | "I", 63 | ] 64 | 65 | [tool.ruff.lint.isort] 66 | known-first-party = ["src", "tests"] 67 | 68 | [tool.ruff.lint.pydocstyle] 69 | convention = "google" 70 | 71 | [tool.mypy] 72 | python_version = "3.13" 73 | strict = true 74 | disallow_untyped_defs = true 75 | disallow_any_generics = true 76 | warn_redundant_casts = true 77 | warn_unused_ignores = true 78 | warn_return_any = true 79 | warn_unreachable = true 80 | show_error_codes = true 81 | namespace_packages = true 82 | explicit_package_bases = true 83 | mypy_path = "src" 84 | 85 | [tool.pytest.ini_options] 86 | addopts = "--strict-markers" 87 | # "function", "class", "module", "package", "session" 88 | asyncio_default_fixture_loop_scope = "function" 89 | pythonpath = ["src"] 90 | testpaths = ["tests/"] 91 | 92 | [tool.coverage] 93 | [tool.coverage.run] 94 | include = [ 95 | "tests/**/*.py", 96 | ] 97 | # omit = [] 98 | # branch = true 99 | 100 | [tool.coverage.report] 101 | show_missing = true 102 | exclude_lines = [ 103 | # 'pragma: no cover', 104 | 'raise AssertionError', 105 | 'raise NotImplementedError', 106 | ] 107 | omit = [ 108 | 'env/*', 109 | 'venv/*', 110 | '.venv/*', 111 | '*/virtualenv/*', 112 | '*/virtualenvs/*', 113 | '*/tests/*', 114 | ] 115 | 116 | [tool.bumpversion] 117 | current_version = "0.0.2" 118 | parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" 119 | serialize = ["{major}.{minor}.{patch}"] 120 | commit = true 121 | tag = true 122 | allow_dirty = false 123 | ignore_missing_version = false 124 | sign_tags = false 125 | tag_name = "v{new_version}" 126 | tag_message = "Bump version: {current_version} → {new_version}" 127 | message = "Bump version: {current_version} → {new_version}" 128 | commit_args = "" 129 | 130 | [[tool.bumpversion.files]] 131 | filename = "pyproject.toml" 132 | search = 'version = "{current_version}"' 133 | replace = 'version = "{new_version}"' 134 | 135 | [[tool.bumpversion.files]] 136 | filename = "src/__init__.py" 137 | search = '__version__ = "{current_version}"' 138 | replace = '__version__ = "{new_version}"' 139 | 140 | [[tool.bumpversion.files]] 141 | filename = "README.md" 142 | search = "version-{current_version}-58f4c2" 143 | replace = "version-{new_version}-58f4c2" 144 | 145 | [[tool.bumpversion.files]] 146 | filename = "CHANGELOG.md" 147 | search = """ 148 | ## [Unreleased] 149 | """ 150 | replace = """ 151 | ## [Unreleased] 152 | 153 | ## [{new_version}] - {now:%Y-%m-%d} 154 | """ -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines the application version.""" 2 | 3 | __version__ = "0.0.2" 4 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """Contains the entrypoint to the app.""" 2 | 3 | pass 4 | -------------------------------------------------------------------------------- /src/py.typed: -------------------------------------------------------------------------------- 1 | # PEP 561 – Distributing and Packaging Type Information 2 | # https://peps.python.org/pep-0561/ -------------------------------------------------------------------------------- ================================================ FILE: docs/architecture/sequence_diagram.mermaid ================================================ sequenceDiagram participant User participant feature_DESC_XXX.md participant ClaudeCode participant generate-prp.md participant feature_PRP_XXX.md participant execute-prp.md participant AI Coding Agent User->>/context/features/feature_DESC_XXX.md: Describes feature User->>ClaudeCode: Runs /generate-prp.md /context/features/feature_DESC_XXX.md ClaudeCode->>generate-prp.md: Executes command generate-prp.md->>AI Coding Agent: Reads /context/features/feature_DESC_XXX.md, codebase, and web AI Coding Agent-->>generate-prp.md: Researches codebase, docs, and examples generate-prp.md->>/context/features/feature_PRP_XXX.md: Generates PRP ClaudeCode->>User: PRP saved in /context/PRPs/feature_PRP_XXX.md User->>ClaudeCode: Runs /execute-prp /context/PRPs/feature_PRP_XXX.md ClaudeCode->>execute-prp.md: Executes command execute-prp.md->>AI Coding Agent: Reads /context/PRPs/feature_PRP_XXX.md and CLAUDE.md AI Coding Agent->>AI Coding Agent: Creates implementation plan, executes, validates, and iterates AI Coding Agent-->>User: Implements feature ================================================ FILE: examples/mcp-server-client/README.md ================================================ # MCP Server Client Example This is an example implementation of an MCP (Message Control Protocol) server and client with a Streamlit GUI. ## Features - MCP server with tools for date/time, dice rolling, and weather - MCP client for interacting with the server - Streamlit GUI for user interaction ## Usage Run the GUI: ```bash make run_gui ``` Run the CLI: ```bash make run_cli ``` ## Screenshots
Connect MCP Connect MCP Connect MCP
MCP connected MCP connected MCP connected
================================================ FILE: examples/mcp-server-client/Makefile ================================================ # MCP Server-Client Example Makefile # This Makefile provides commands for the MCP server-client example .SILENT: .ONESHELL: .PHONY: setup_dev setup_prod ruff test_all check_types coverage_all run_gui run_server run_client help .DEFAULT_GOAL := help SRC_PATH := src APP_PATH := $(SRC_PATH) # MARK: setup setup_dev: ## Install uv and development dependencies echo "Setting up dev environment ..." pip install uv -q uv sync --all-groups setup_prod: ## Install uv and production dependencies echo "Setting up prod environment ..." pip install uv -q uv sync --frozen # MARK: code quality ruff: ## Format and lint code with ruff uv run ruff format uv run ruff check --fix test_all: ## Run all tests uv run pytest coverage_all: ## Get test coverage uv run coverage run -m pytest || true uv run coverage report -m check_types: ## Check for static typing errors uv run mypy $(APP_PATH) # MARK: run run_gui: ## Launch Streamlit GUI uv run python -m src.main gui $(ARGS) run_server: ## Run MCP server uv run python -m src.main server run_client: ## Run MCP client (requires ARGS) uv run python -m src.main client $(ARGS) # MARK: help help: ## Display available commands echo "Usage: make [command]" echo "Commands:" awk '/^[a-zA-Z0-9_-]+:.*?##/ { helpMessage = match($$0, /## (.*)/) if (helpMessage) { recipe = $$1 sub(/:/, "", recipe) printf " \033[36m%-20s\033[0m %s\n", recipe, substr($$0, RSTART + 3, RLENGTH) } }' $(MAKEFILE_LIST) ================================================ FILE: examples/mcp-server-client/pyproject.toml ================================================ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] version = "0.0.1" name = "context-engineering-template" description = "Assess the effectiveness of agentic AI systems across various use cases focusing on agnostic metrics that measure core agentic capabilities." authors = [ {name = "qte77", email = "qte@77.gh"} ] readme = "README.md" requires-python = "==3.13.*" license = "bsd-3-clause" dependencies = [ "mcp[cli]>=1.10.0", "httpx>=0.25.0", "pydantic>=2.0.0", "streamlit>=1.28.0", ] # [project.urls] # Documentation = "" [dependency-groups] dev = [ "mypy>=1.16.0", "ruff>=0.11.12", ] test = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", "pytest-httpx>=0.28.0", ] docs = [ "griffe>=1.5.1", "mkdocs>=1.6.1", "mkdocs-awesome-pages-plugin>=2.9.3", "mkdocs-gen-files>=0.5.0", "mkdocs-literate-nav>=0.6.1", "mkdocs-material>=9.5.44", "mkdocs-section-index>=0.3.8", "mkdocstrings[python]>=0.27.0", ] [tool.uv] package = true exclude-newer = "2025-07-06T00:00:00Z" [tool.hatch.build.targets.wheel] only-include = ["/README.md"] [tool.hatch.build.targets.sdist] include = ["/README.md", "/Makefile", "/tests"] [tool.ruff] target-version = "py313" src = ["src", "tests"] [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] # ignore = ["E203"] # Whitespace before ':' unfixable = ["B"] select = [ # pycodestyle "E", # Pyflakes "F", # pyupgrade "UP", # isort "I", ] [tool.ruff.lint.isort] known-first-party = ["src", "tests"] [tool.ruff.lint.pydocstyle] convention = "google" [tool.mypy] python_version = "3.13" strict = true disallow_untyped_defs = true disallow_any_generics = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = true warn_unreachable = true show_error_codes = true namespace_packages = true explicit_package_bases = true mypy_path = "src" [tool.pytest.ini_options] addopts = "--strict-markers" # "function", "class", "module", "package", "session" asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] testpaths = ["tests/"] [tool.coverage] [tool.coverage.run] include = [ "tests/**/*.py", ] # omit = [] # branch = true [tool.coverage.report] show_missing = true exclude_lines = [ # 'pragma: no cover', 'raise AssertionError', 'raise NotImplementedError', ] omit = [ 'env/*', 'venv/*', '.venv/*', '*/virtualenv/*', '*/virtualenvs/*', '*/tests/*', ] [tool.bumpversion] current_version = "0.0.1" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] commit = true tag = true allow_dirty = false ignore_missing_version = false sign_tags = false tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" message = "Bump version: {current_version} → {new_version}" commit_args = "" [[tool.bumpversion.files]] filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' [[tool.bumpversion.files]] filename = "src/__init__.py" search = '__version__ = "{current_version}"' replace = '__version__ = "{new_version}"' [[tool.bumpversion.files]] filename = "README.md" search = "version-{current_version}-58f4c2" replace = "version-{new_version}-58f4c2" [[tool.bumpversion.files]] filename = "CHANGELOG.md" search = """ ## [Unreleased] """ replace = """ ## [Unreleased] ## [{new_version}] - {now:%Y-%m-%d} """ ================================================ FILE: examples/mcp-server-client/uv.lock ================================================ version = 1 revision = 2 requires-python = "==3.13.*" [options] exclude-newer = "2025-07-06T00:00:00Z" [[package]] name = "altair" version = "5.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "jsonschema" }, { name = "narwhals" }, { name = "packaging" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305, upload-time = "2024-11-23T23:39:58.542Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200, upload-time = "2024-11-23T23:39:56.4Z" }, ] [[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, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backrefs" version = "5.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "bracex" version = "2.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] [[package]] name = "cachetools" version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" }, ] [[package]] name = "certifi" version = "2025.6.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[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, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "context-engineering-template" version = "0.0.1" source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "mcp", extra = ["cli"] }, { name = "pydantic" }, { name = "streamlit" }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "ruff" }, ] docs = [ { name = "griffe" }, { name = "mkdocs" }, { name = "mkdocs-awesome-pages-plugin" }, { name = "mkdocs-gen-files" }, { name = "mkdocs-literate-nav" }, { name = "mkdocs-material" }, { name = "mkdocs-section-index" }, { name = "mkdocstrings", extra = ["python"] }, ] test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-httpx" }, ] [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.25.0" }, { name = "mcp", extras = ["cli"], specifier = ">=1.10.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "streamlit", specifier = ">=1.28.0" }, ] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.16.0" }, { name = "ruff", specifier = ">=0.11.12" }, ] docs = [ { name = "griffe", specifier = ">=1.5.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-awesome-pages-plugin", specifier = ">=2.9.3" }, { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, { name = "mkdocs-literate-nav", specifier = ">=0.6.1" }, { name = "mkdocs-material", specifier = ">=9.5.44" }, { name = "mkdocs-section-index", specifier = ">=0.3.8" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0" }, ] test = [ { name = "pytest", specifier = ">=7.0.0" }, { name = "pytest-asyncio", specifier = ">=0.21.0" }, { name = "pytest-httpx", specifier = ">=0.28.0" }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] name = "gitpython" version = "3.1.44" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, ] [[package]] name = "griffe" version = "1.7.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[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, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, ] [[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, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jsonschema" version = "4.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] name = "markdown" version = "3.8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "mcp" version = "1.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969, upload-time = "2025-06-27T12:03:08.982Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878, upload-time = "2025-06-27T12:03:07.328Z" }, ] [package.optional-dependencies] cli = [ { name = "python-dotenv" }, { name = "typer" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, ] [[package]] name = "mkdocs-awesome-pages-plugin" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "natsort" }, { name = "wcmatch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/e8/6ae9c18d8174a5d74ce4ade7a7f4c350955063968bc41ff1e5833cff4a2b/mkdocs_awesome_pages_plugin-2.10.1.tar.gz", hash = "sha256:cda2cb88c937ada81a4785225f20ef77ce532762f4500120b67a1433c1cdbb2f", size = 16303, upload-time = "2024-12-22T21:13:49.19Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/61/19fc1e9c579dbfd4e8a402748f1d63cab7aabe8f8d91eb0235e45b32d040/mkdocs_awesome_pages_plugin-2.10.1-py3-none-any.whl", hash = "sha256:c6939dbea37383fc3cf8c0a4e892144ec3d2f8a585e16fdc966b34e7c97042a7", size = 15118, upload-time = "2024-12-22T21:13:46.945Z" }, ] [[package]] name = "mkdocs-gen-files" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/85/2d634462fd59136197d3126ca431ffb666f412e3db38fd5ce3a60566303e/mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc", size = 7539, upload-time = "2023-04-27T19:48:04.894Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/0f/1e55b3fd490ad2cecb6e7b31892d27cb9fc4218ec1dab780440ba8579e74/mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea", size = 8380, upload-time = "2023-04-27T19:48:07.059Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] name = "mkdocs-literate-nav" version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" }, ] [[package]] name = "mkdocs-material" version = "9.6.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocs-section-index" version = "0.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/40/4aa9d3cfa2ac6528b91048847a35f005b97ec293204c02b179762a85b7f2/mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", size = 14446, upload-time = "2025-04-05T20:56:45.387Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/53/76c109e6f822a6d19befb0450c87330b9a6ce52353de6a9dda7892060a1f/mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776", size = 8796, upload-time = "2025-04-05T20:56:43.975Z" }, ] [[package]] name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, ] [package.optional-dependencies] python = [ { name = "mkdocstrings-python" }, ] [[package]] name = "mkdocstrings-python" version = "1.16.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, ] [[package]] name = "mypy" version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "narwhals" version = "1.45.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/9f/284886c5cea849b4ed1c55babcb48cb1084886139e8ac31e9849112ce6d0/narwhals-1.45.0.tar.gz", hash = "sha256:f9ecefb9d09cda6fefa8ead10dc37a79129b6c78b0ac7117d21b4d4486bdd0d1", size = 508812, upload-time = "2025-07-01T11:26:07.114Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/a4/337a229d184b23ee63e6b730ac1588d77067af77c550dbf69cf1d74c3298/narwhals-1.45.0-py3-none-any.whl", hash = "sha256:0585612aa7ec89f9d061e78410b6fb8772794389d1a29d5799572d6b81999497", size = 371633, upload-time = "2025-07-01T11:26:05.409Z" }, ] [[package]] name = "natsort" version = "8.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, ] [[package]] name = "numpy" version = "2.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" }, { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" }, { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" }, { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" }, { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" }, { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" }, { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" }, { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" }, { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" }, { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" }, { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" }, { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" }, { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" }, { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" }, { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" }, { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" }, { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" }, { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" }, { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" }, { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" }, { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" }, { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "pandas" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "protobuf" version = "6.31.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] name = "pyarrow" version = "20.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/aa/daa413b81446d20d4dad2944110dcf4cf4f4179ef7f685dd5a6d7570dc8e/pyarrow-20.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a15532e77b94c61efadde86d10957950392999503b3616b2ffcef7621a002893", size = 30798501, upload-time = "2025-04-27T12:30:48.351Z" }, { url = "https://files.pythonhosted.org/packages/ff/75/2303d1caa410925de902d32ac215dc80a7ce7dd8dfe95358c165f2adf107/pyarrow-20.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dd43f58037443af715f34f1322c782ec463a3c8a94a85fdb2d987ceb5658e061", size = 32277895, upload-time = "2025-04-27T12:30:55.238Z" }, { url = "https://files.pythonhosted.org/packages/92/41/fe18c7c0b38b20811b73d1bdd54b1fccba0dab0e51d2048878042d84afa8/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0d288143a8585806e3cc7c39566407aab646fb9ece164609dac1cfff45f6ae", size = 41327322, upload-time = "2025-04-27T12:31:05.587Z" }, { url = "https://files.pythonhosted.org/packages/da/ab/7dbf3d11db67c72dbf36ae63dcbc9f30b866c153b3a22ef728523943eee6/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6953f0114f8d6f3d905d98e987d0924dabce59c3cda380bdfaa25a6201563b4", size = 42411441, upload-time = "2025-04-27T12:31:15.675Z" }, { url = "https://files.pythonhosted.org/packages/90/c3/0c7da7b6dac863af75b64e2f827e4742161128c350bfe7955b426484e226/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:991f85b48a8a5e839b2128590ce07611fae48a904cae6cab1f089c5955b57eb5", size = 40677027, upload-time = "2025-04-27T12:31:24.631Z" }, { url = "https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b", size = 42281473, upload-time = "2025-04-27T12:31:31.311Z" }, { url = "https://files.pythonhosted.org/packages/bc/0b/d56c63b078876da81bbb9ba695a596eabee9b085555ed12bf6eb3b7cab0e/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b71daf534f4745818f96c214dbc1e6124d7daf059167330b610fc69b6f3d3e3", size = 42893897, upload-time = "2025-04-27T12:31:39.406Z" }, { url = "https://files.pythonhosted.org/packages/92/ac/7d4bd020ba9145f354012838692d48300c1b8fe5634bfda886abcada67ed/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8b88758f9303fa5a83d6c90e176714b2fd3852e776fc2d7e42a22dd6c2fb368", size = 44543847, upload-time = "2025-04-27T12:31:45.997Z" }, { url = "https://files.pythonhosted.org/packages/9d/07/290f4abf9ca702c5df7b47739c1b2c83588641ddfa2cc75e34a301d42e55/pyarrow-20.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:30b3051b7975801c1e1d387e17c588d8ab05ced9b1e14eec57915f79869b5031", size = 25653219, upload-time = "2025-04-27T12:31:54.11Z" }, { url = "https://files.pythonhosted.org/packages/95/df/720bb17704b10bd69dde086e1400b8eefb8f58df3f8ac9cff6c425bf57f1/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ca151afa4f9b7bc45bcc791eb9a89e90a9eb2772767d0b1e5389609c7d03db63", size = 30853957, upload-time = "2025-04-27T12:31:59.215Z" }, { url = "https://files.pythonhosted.org/packages/d9/72/0d5f875efc31baef742ba55a00a25213a19ea64d7176e0fe001c5d8b6e9a/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:4680f01ecd86e0dd63e39eb5cd59ef9ff24a9d166db328679e36c108dc993d4c", size = 32247972, upload-time = "2025-04-27T12:32:05.369Z" }, { url = "https://files.pythonhosted.org/packages/d5/bc/e48b4fa544d2eea72f7844180eb77f83f2030b84c8dad860f199f94307ed/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4c8534e2ff059765647aa69b75d6543f9fef59e2cd4c6d18015192565d2b70", size = 41256434, upload-time = "2025-04-27T12:32:11.814Z" }, { url = "https://files.pythonhosted.org/packages/c3/01/974043a29874aa2cf4f87fb07fd108828fc7362300265a2a64a94965e35b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1f8a47f4b4ae4c69c4d702cfbdfe4d41e18e5c7ef6f1bb1c50918c1e81c57b", size = 42353648, upload-time = "2025-04-27T12:32:20.766Z" }, { url = "https://files.pythonhosted.org/packages/68/95/cc0d3634cde9ca69b0e51cbe830d8915ea32dda2157560dda27ff3b3337b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a1f60dc14658efaa927f8214734f6a01a806d7690be4b3232ba526836d216122", size = 40619853, upload-time = "2025-04-27T12:32:28.1Z" }, { url = "https://files.pythonhosted.org/packages/29/c2/3ad40e07e96a3e74e7ed7cc8285aadfa84eb848a798c98ec0ad009eb6bcc/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:204a846dca751428991346976b914d6d2a82ae5b8316a6ed99789ebf976551e6", size = 42241743, upload-time = "2025-04-27T12:32:35.792Z" }, { url = "https://files.pythonhosted.org/packages/eb/cb/65fa110b483339add6a9bc7b6373614166b14e20375d4daa73483755f830/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f3b117b922af5e4c6b9a9115825726cac7d8b1421c37c2b5e24fbacc8930612c", size = 42839441, upload-time = "2025-04-27T12:32:46.64Z" }, { url = "https://files.pythonhosted.org/packages/98/7b/f30b1954589243207d7a0fbc9997401044bf9a033eec78f6cb50da3f304a/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e724a3fd23ae5b9c010e7be857f4405ed5e679db5c93e66204db1a69f733936a", size = 44503279, upload-time = "2025-04-27T12:32:56.503Z" }, { url = "https://files.pythonhosted.org/packages/37/40/ad395740cd641869a13bcf60851296c89624662575621968dcfafabaa7f6/pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9", size = 25944982, upload-time = "2025-04-27T12:33:04.72Z" }, ] [[package]] name = "pydantic" version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] [[package]] name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] [[package]] name = "pydantic-settings" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] [[package]] name = "pydeck" version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pymdown-extensions" version = "10.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, ] [[package]] name = "pytest" version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-asyncio" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, ] [[package]] name = "pytest-httpx" version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "referencing" version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] name = "requests" version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "rich" version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] name = "rpds-py" version = "0.26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" }, { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" }, { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" }, { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" }, { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" }, { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" }, { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" }, { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" }, { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" }, { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" }, { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" }, { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" }, { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" }, { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" }, { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" }, { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" }, { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" }, { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" }, { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" }, { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" }, { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" }, { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" }, { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, ] [[package]] name = "ruff" version = "0.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" }, { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" }, { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" }, { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" }, { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" }, { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" }, { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" }, { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" }, { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" }, { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" }, { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" }, { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" }, { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" }, { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" }, { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" }, { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" }, { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] [[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, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "sse-starlette" version = "2.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" }, ] [[package]] name = "starlette" version = "0.47.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, ] [[package]] name = "streamlit" version = "1.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altair" }, { name = "blinker" }, { name = "cachetools" }, { name = "click" }, { name = "gitpython" }, { name = "numpy" }, { name = "packaging" }, { name = "pandas" }, { name = "pillow" }, { name = "protobuf" }, { name = "pyarrow" }, { name = "pydeck" }, { name = "requests" }, { name = "tenacity" }, { name = "toml" }, { name = "tornado" }, { name = "typing-extensions" }, { name = "watchdog", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/7e/c7499c447b7021657fa5c851091072c1c7a26f1dda0369c21c0243fc35e7/streamlit-1.46.1.tar.gz", hash = "sha256:2cc4ad01cfeded9ad953a21829eb879b90aa77af4068c68397411c2d5c8862cf", size = 9651018, upload-time = "2025-06-26T16:03:05.66Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/3b/35400175788cdd6a43c90dce1e7f567eb6843a3ba0612508c0f19ee31f5f/streamlit-1.46.1-py3-none-any.whl", hash = "sha256:dffa373230965f87ccc156abaff848d7d731920cf14106f3b99b1ea18076f728", size = 10051346, upload-time = "2025-06-26T16:03:02.934Z" }, ] [[package]] name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] name = "tornado" version = "6.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, ] [[package]] name = "typer" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] name = "uvicorn" version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wcmatch" version = "10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, ] ================================================ FILE: examples/mcp-server-client/context/features/feature_1_mcp_server.md ================================================ # Feature description for: MCP Server with tools ## FEATURE Implement an **MCP (Message Control Protocol) Server** in Python that exposes three callable tools via structured messages. The server should receive well-formed MCP messages and dispatch tool invocations accordingly. The three tools to be exposed are: 1. **Roll Dice**: Accepts a format like `2d6` or `1d20` and returns the rolled values and total. 2. **Get Weather**: Accepts a city name or coordinates and returns the current weather conditions using a public weather API. 3. **Get Date**: Returns the current date and time in ISO 8601 format or based on a requested timezone. The server should be modular, testable, and extensible for future tools. Logging, error handling, and message validation should be considered first-class concerns. ## EXAMPLES Located in `/context/examples`: * `roll_dice_example.json`: Demonstrates sending `{"tool": "roll_dice", "args": {"notation": "3d6"}}` and receiving `{"result": {"values": [4,2,6], "total": 12}}`. * `get_weather_example.json`: Sends `{"tool": "get_weather", "args": {"location": "San Francisco"}}` and expects weather data such as temperature, condition, and wind speed. * `get_date_example.json`: Sends `{"tool": "get_date", "args": {"timezone": "UTC"}}` and receives `{"result": "2025-07-06T16:22:00Z"}`. These examples cover correct usage and malformed inputs to validate tool response and error handling. ## DOCUMENTATION * [Open-Meteo API](https://open-meteo.com/en/docs): For retrieving weather information. * [Python `datetime` module](https://docs.python.org/3/library/datetime.html): For implementing date and time tool. * [random module (Python)](https://docs.python.org/3/library/random.html): For rolling dice. * \[MCP Protocol Overview (proprietary/internal if applicable)] or general protocol documentation, if using a specific spec. Additional context from [context-engineering-intro](https://github.com/qte77/context-engineering-template) will inform message structure and processing strategy. ## OTHER CONSIDERATIONS * **Tool routing logic** should be clearly separated to allow clean expansion. * **Input validation** is critical: especially for `roll_dice`, invalid formats (e.g., `3x5`, `0d6`, `d10`) must return informative errors. * **Weather API failures or rate limits** should be gracefully handled with fallback messages. * **Timezone parsing** for `get_date` should use `pytz` or `zoneinfo`, and clearly inform users when timezones are unsupported. * **Security note**: Weather and date APIs should not expose sensitive request metadata or leak internal server details in errors. * AI coding assistants often: * Miss edge case handling (e.g., zero dice, negative sides) * Forget to structure results consistently across tools * Fail to modularize tool logic, making future expansion difficult ================================================ FILE: examples/mcp-server-client/context/features/feature_2_mcp_client.md ================================================ # Feature description for: MCP Client for Tool Invocation ## FEATURE Implement a **Python-based MCP Client** capable of sending structured requests to an MCP Server and handling the corresponding responses. The client should: * Connect to the MCP server over a socket, HTTP, or another configured protocol. * Serialize requests into the expected MCP message format (e.g., JSON or line-based protocol). * Provide a command-line interface (CLI) and/or programmatic interface for interacting with the following tools: 1. **Roll Dice** (`roll_dice`) – accepts dice notation like `2d6`, `1d20`. 2. **Get Weather** (`get_weather`) – accepts a location name or coordinates. 3. **Get Date** (`get_date`) – optionally accepts a timezone. The client should also handle connection errors, invalid tool responses, and retry logic gracefully. ## EXAMPLES Located in `/context/examples`: * `client_roll_dice_input.json`: `{ "tool": "roll_dice", "args": { "notation": "2d6" } }` * `client_get_weather_input.json`: `{ "tool": "get_weather", "args": { "location": "Berlin" } }` * `client_get_date_input.json`: `{ "tool": "get_date", "args": { "timezone": "UTC" } }` * `client_invalid_tool.json`: `{ "tool": "fly_to_mars", "args": {} }` → Should trigger a meaningful error from the server These example requests and expected responses can be used for local testing and automated integration checks. ## DOCUMENTATION * [Python `socket` module](https://docs.python.org/3/library/socket.html) or [requests](https://docs.python.org/3/library/urllib.request.html) depending on transport. * [JSON module](https://docs.python.org/3/library/json.html) for message formatting. * [argparse](https://docs.python.org/3/library/argparse.html) for implementing a simple CLI wrapper. * Reference the MCP Server protocol spec or internal documentation (e.g. *MCP Protocol Overview* if proprietary). * [context-engineering-template](https://github.com/qte/context-engineering-template) for usage conventions. ## OTHER CONSIDERATIONS * Client must validate outgoing messages before sending to avoid malformed requests. * Handle connection errors, timeouts, and retries in a user-friendly manner. * The response handler should check for required fields (`result`, `error`, etc.) to avoid crashes on malformed server responses. * Consider pluggability of tools so future expansions can be supported with minimal refactoring. * AI assistants often: * Miss error handling around partial or no server responses. * Forget to properly close socket connections or handle timeouts. * Write overly rigid request builders, making CLI usage frustrating. ================================================ FILE: examples/mcp-server-client/context/features/feature_3_streamlit_gui.md ================================================ # Feature description for: Streamlit GUI for MCP Server-Client Interaction Showcase ## FEATURE Develop a **Streamlit-based graphical user interface (GUI)** to demonstrate and interactively showcase the communication and integration between the MCP Server and MCP Client. The GUI should allow users to: * Select and invoke any of the three available tools (`roll_dice`, `get_weather`, `get_date`) via intuitive form inputs. * Enter tool-specific parameters such as dice notation, location, or timezone. * Display real-time request payloads sent by the client and the corresponding responses received from the server. * Handle and display error messages gracefully. * Log interaction history for the current session, allowing users to review previous commands and results. * Provide clear visual feedback about the status of the connection and request execution. This GUI acts as both a testing ground and demonstration interface, useful for users unfamiliar with command-line tools or raw protocol messages. ## EXAMPLES Located in `/context/examples`: * `streamlit_roll_dice_interaction.json`: Example input/output pairs demonstrating a dice roll session in the GUI. * `streamlit_get_weather_interaction.json`: Demonstrates user inputs for location and the displayed weather response. * `streamlit_get_date_interaction.json`: Shows date/time requests with optional timezone selection. * `streamlit_error_handling.json`: Examples of how the GUI displays server-side validation errors or connection issues. These examples serve as test cases for GUI input validation and response rendering. ## DOCUMENTATION * [Streamlit Documentation](https://docs.streamlit.io/) for building interactive Python apps. * \[MCP Server and Client Protocol Specs] (internal/proprietary or from context-engineering-intro). * Python libraries for HTTP or socket communication used by the client. * UI/UX design best practices for interactive demos. * [context-engineering-intro](https://github.com/coleam00/context-engineering-intro) for project conventions. ## OTHER CONSIDERATIONS * Ensure asynchronous or non-blocking communication so the UI remains responsive during server interactions. * Validate inputs in the GUI before sending to the client to minimize server errors. * Provide helpful tooltips or inline help to explain tool parameters to users unfamiliar with dice notation or timezone formats. * Consider session state management in Streamlit to maintain history and status. * AI coding assistants often overlook proper error propagation to the UI and user-friendly messaging. * Security considerations: if exposing any sensitive endpoints or API keys, avoid hardcoding secrets in the GUI code. * Design with extensibility in mind to add new tools or more complex workflows easily. ================================================ FILE: examples/mcp-server-client/context/outputs/client_get_date_input.json ================================================ { "description": "Simple client input format for get_date tool", "cli_command": "python -m src.main client --server src/mcp_server/server.py get_date --timezone UTC", "expected_input": { "tool": "get_date", "arguments": { "timezone": "UTC" } }, "expected_output_format": { "success": true, "tool_name": "get_date", "result": { "content": [ { "type": "text", "text": "🕐 **Current Date & Time**\n📅 Date: **2025-07-07** (Monday)\n⏰ Time: **14:30:25**\n🌍 Timezone: **UTC**\n📋 ISO 8601: `2025-07-07T14:30:25+00:00`\n🔢 Unix Timestamp: `1720360225`" } ] } }, "examples": [ { "timezone": "America/New_York", "description": "Get Eastern Time" }, { "timezone": "Europe/London", "description": "Get London time" }, { "timezone": "Asia/Tokyo", "description": "Get Tokyo time" }, { "timezone": "pst", "description": "Get Pacific Time using alias" } ] } ================================================ FILE: examples/mcp-server-client/context/outputs/client_get_weather_input.json ================================================ { "description": "Simple client input format for get_weather tool", "cli_command": "python -m src.main client --server src/mcp_server/server.py get_weather --location 'San Francisco'", "expected_input": { "tool": "get_weather", "arguments": { "location": "San Francisco" } }, "expected_output_format": { "success": true, "tool_name": "get_weather", "result": { "content": [ { "type": "text", "text": "🌤️ **Weather for San Francisco**\n🌡️ Temperature: **18.5°C**\n☁️ Condition: **Partly cloudy**\n💨 Wind Speed: **12.3 km/h**\n💧 Humidity: **65%**\n🕐 Updated: 2025-07-07 14:30 UTC" } ] } }, "examples": [ { "location": "London", "description": "Get weather for London" }, { "location": "New York", "description": "Get weather for New York" }, { "location": "37.7749,-122.4194", "description": "Get weather using coordinates" } ] } ================================================ FILE: examples/mcp-server-client/context/outputs/client_invalid_tool.json ================================================ { "description": "Error handling example for invalid tool", "cli_command": "python -m src.main client --server src/mcp_server/server.py invalid_tool --arg value", "expected_input": { "tool": "invalid_tool", "arguments": { "arg": "value" } }, "expected_output_format": { "success": false, "tool_name": "invalid_tool", "error": "Tool 'invalid_tool' not available. Available tools: ['roll_dice', 'get_weather', 'get_date']", "arguments": { "arg": "value" } }, "error_scenarios": [ { "scenario": "Tool not available", "tool": "nonexistent_tool", "expected_error": "Tool 'nonexistent_tool' not available" }, { "scenario": "Server not running", "server_path": "./nonexistent_server.py", "expected_error": "Server script not found" }, { "scenario": "Invalid server path", "server_path": "/dev/null", "expected_error": "Failed to connect to server" }, { "scenario": "Connection timeout", "timeout": 1, "expected_error": "Connection timeout" } ] } ================================================ FILE: examples/mcp-server-client/context/outputs/client_roll_dice_input.json ================================================ { "description": "Simple client input format for roll_dice tool", "cli_command": "python -m src.main client --server src/mcp_server/server.py roll_dice --notation 2d6", "expected_input": { "tool": "roll_dice", "arguments": { "notation": "2d6" } }, "expected_output_format": { "success": true, "tool_name": "roll_dice", "result": { "content": [ { "type": "text", "text": "🎲 Rolled 2d6: [3, 5] = **8**" } ] } }, "examples": [ { "notation": "1d20", "description": "Roll a 20-sided die" }, { "notation": "3d6", "description": "Roll three 6-sided dice" }, { "notation": "2d10", "description": "Roll two 10-sided dice" } ] } ================================================ FILE: examples/mcp-server-client/context/outputs/get_date_example.json ================================================ { "description": "Example request and response for the get_date tool", "request": { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_date", "arguments": { "timezone": "UTC" } }, "id": 3 }, "response": { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "🕐 **Current Date & Time**\n📅 Date: **2025-07-07** (Monday)\n⏰ Time: **14:30:25**\n🌍 Timezone: **UTC**\n📋 ISO 8601: `2025-07-07T14:30:25+00:00`\n🔢 Unix Timestamp: `1720360225`" } ], "isError": false } }, "examples": { "valid_requests": [ { "timezone": "UTC", "description": "Coordinated Universal Time" }, { "timezone": "America/New_York", "description": "Eastern Time" }, { "timezone": "America/Los_Angeles", "description": "Pacific Time" }, { "timezone": "Europe/London", "description": "British Time" }, { "timezone": "Asia/Tokyo", "description": "Japan Standard Time" }, { "timezone": "est", "description": "Eastern Time alias" }, { "timezone": "pst", "description": "Pacific Time alias" } ], "invalid_requests": [ { "timezone": "Invalid/Timezone", "error": "Invalid timezone: 'Invalid/Timezone'. Common timezones: UTC, America/New_York, America/Los_Angeles, America/Chicago, Europe/London, Europe/Paris, Asia/Tokyo, Australia/Sydney. Aliases: utc, gmt, est, pst, cst, mst, edt, pdt, cdt, mdt, bst, cet, jst, aest. Use IANA timezone names (e.g., 'America/New_York') or aliases." }, { "timezone": "", "error": "Timezone cannot be empty" } ] }, "supported_timezones": { "aliases": { "utc": "UTC", "gmt": "UTC", "est": "America/New_York", "pst": "America/Los_Angeles", "cst": "America/Chicago", "mst": "America/Denver", "edt": "America/New_York", "pdt": "America/Los_Angeles", "cdt": "America/Chicago", "mdt": "America/Denver", "bst": "Europe/London", "cet": "Europe/Paris", "jst": "Asia/Tokyo", "aest": "Australia/Sydney" }, "common_iana_zones": [ "UTC", "America/New_York", "America/Los_Angeles", "America/Chicago", "America/Denver", "Europe/London", "Europe/Paris", "Europe/Berlin", "Asia/Tokyo", "Asia/Shanghai", "Australia/Sydney" ] } } ================================================ FILE: examples/mcp-server-client/context/outputs/get_weather_example.json ================================================ { "description": "Example request and response for the get_weather tool", "request": { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "San Francisco" } }, "id": 2 }, "response": { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "🌤️ **Weather for San Francisco**\n🌡️ Temperature: **18.5°C**\n☁️ Condition: **Partly cloudy**\n💨 Wind Speed: **12.3 km/h**\n💧 Humidity: **65%**\n🕐 Updated: 2025-07-07 14:30 UTC" } ], "isError": false } }, "examples": { "valid_requests": [ { "location": "London", "description": "Weather for London city" }, { "location": "New York", "description": "Weather for New York city" }, { "location": "37.7749,-122.4194", "description": "Weather using coordinates (San Francisco)" }, { "location": "Tokyo", "description": "Weather for Tokyo city" } ], "invalid_requests": [ { "location": "Unknown City", "error": "Unknown location: 'Unknown City'. Please use coordinates (lat,lon) or one of: berlin, beijing, cairo, chicago, lagos, london, los angeles, madrid, miami, moscow, mumbai, new york, paris, rome, san francisco, seattle, sydney, tokyo, toronto, vancouver" }, { "location": "", "error": "Location cannot be empty" }, { "location": "999,999", "error": "Unknown location: '999,999'. Please use coordinates (lat,lon) or one of: [city list]" } ] }, "supported_cities": [ "San Francisco", "New York", "London", "Paris", "Tokyo", "Sydney", "Los Angeles", "Chicago", "Miami", "Seattle", "Vancouver", "Toronto", "Berlin", "Rome", "Madrid", "Moscow", "Beijing", "Mumbai", "Cairo", "Lagos" ] } ================================================ FILE: examples/mcp-server-client/context/outputs/roll_dice_example.json ================================================ { "description": "Example request and response for the roll_dice tool", "request": { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "roll_dice", "arguments": { "notation": "3d6" } }, "id": 1 }, "response": { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "🎲 Rolled 3d6: [4, 2, 6] = **12**" } ], "isError": false } }, "examples": { "valid_requests": [ { "notation": "1d20", "description": "Single twenty-sided die" }, { "notation": "2d6", "description": "Two six-sided dice" }, { "notation": "4d10", "description": "Four ten-sided dice" } ], "invalid_requests": [ { "notation": "d6", "error": "Invalid dice notation: 'd6'. Expected format: 'XdY' (e.g., '2d6', '1d20')" }, { "notation": "0d6", "error": "Dice count must be greater than 0" }, { "notation": "101d6", "error": "Dice count must not exceed 100" }, { "notation": "1d0", "error": "Number of sides must be greater than 0" }, { "notation": "abc", "error": "Invalid dice notation: 'abc'. Expected format: 'XdY' (e.g., '2d6', '1d20')" } ] } } ================================================ FILE: examples/mcp-server-client/context/outputs/streamlit_error_handling.json ================================================ { "interaction_type": "streamlit_gui", "tool_name": "roll_dice", "timestamp": "2024-01-15T10:36:00Z", "request": { "method": "tool_invocation", "parameters": { "tool": "roll_dice", "arguments": { "notation": "invalid_dice" } } }, "response": { "success": false, "result": null, "error": "Invalid dice notation format: invalid_dice", "tool_name": "roll_dice", "arguments": { "notation": "invalid_dice" } }, "execution_time": 0.05, "gui_state": { "connected": true, "server_path": "src/mcp_server/server.py", "available_tools": ["roll_dice", "get_weather", "get_date"] }, "error_details": { "validation_error": "Dice notation must follow pattern: NdN (e.g., 2d6, 1d20)", "user_input": "invalid_dice", "suggested_corrections": ["2d6", "1d20", "3d10"] } } ================================================ FILE: examples/mcp-server-client/context/outputs/streamlit_get_date_interaction.json ================================================ { "interaction_type": "streamlit_gui", "tool_name": "get_date", "timestamp": "2024-01-15T10:34:00Z", "request": { "method": "tool_invocation", "parameters": { "tool": "get_date", "arguments": { "timezone": "America/New_York" } } }, "response": { "success": true, "result": { "datetime": "2024-01-15T05:34:15-05:00", "timezone": "America/New_York", "formatted": "Monday, January 15, 2024 at 5:34:15 AM EST", "unix_timestamp": 1705313655 }, "tool_name": "get_date", "arguments": { "timezone": "America/New_York" } }, "execution_time": 0.12, "gui_state": { "connected": true, "server_path": "src/mcp_server/server.py", "available_tools": ["roll_dice", "get_weather", "get_date"] } } ================================================ FILE: examples/mcp-server-client/context/outputs/streamlit_get_weather_interaction.json ================================================ { "interaction_type": "streamlit_gui", "tool_name": "get_weather", "timestamp": "2024-01-15T10:32:00Z", "request": { "method": "tool_invocation", "parameters": { "tool": "get_weather", "arguments": { "location": "San Francisco" } } }, "response": { "success": true, "result": { "location": "San Francisco, CA", "temperature": "18°C", "condition": "Partly cloudy", "humidity": "65%", "wind": "12 mph NW" }, "tool_name": "get_weather", "arguments": { "location": "San Francisco" } }, "execution_time": 0.45, "gui_state": { "connected": true, "server_path": "src/mcp_server/server.py", "available_tools": ["roll_dice", "get_weather", "get_date"] } } ================================================ FILE: examples/mcp-server-client/context/outputs/streamlit_roll_dice_interaction.json ================================================ { "interaction_type": "streamlit_gui", "tool_name": "roll_dice", "timestamp": "2024-01-15T10:30:00Z", "request": { "method": "tool_invocation", "parameters": { "tool": "roll_dice", "arguments": { "notation": "2d6" } } }, "response": { "success": true, "result": { "values": [3, 5], "total": 8, "notation": "2d6" }, "tool_name": "roll_dice", "arguments": { "notation": "2d6" } }, "execution_time": 0.15, "gui_state": { "connected": true, "server_path": "src/mcp_server/server.py", "available_tools": ["roll_dice", "get_weather", "get_date"] } } ================================================ FILE: examples/mcp-server-client/context/PRPs/feature_1_mcp_server.md ================================================ # MCP Server Implementation with Three Core Tools ## Purpose Product Requirements Prompt (PRP) for implementing a complete MCP (Model Context Protocol) server in Python that exposes three callable tools: dice rolling, weather retrieval, and date/time services. This PRP provides comprehensive context for one-pass implementation success. ## Goal Build a production-ready MCP server in Python that: - Implements the MCP protocol for structured message handling - Exposes three tools: `roll_dice`, `get_weather`, and `get_date` - Provides robust error handling and input validation - Follows the project's established patterns and conventions - Is modular, testable, and extensible for future tools ## Why - **Standardization**: Implements the emerging MCP protocol standard for AI tool integration - **Modularity**: Creates a foundation for adding more tools in the future - **User Value**: Provides practical utilities (dice, weather, time) accessible via structured protocol - **Learning**: Demonstrates best practices for building MCP servers in Python ## What ### User-visible behavior - Server accepts MCP protocol messages in JSON format - Tools can be invoked via structured requests like `{"tool": "roll_dice", "args": {"notation": "3d6"}}` - Returns structured responses with results or detailed error messages - Handles malformed inputs gracefully with informative feedback ### Success Criteria - [ ] MCP server starts and accepts connections via stdio transport - [ ] All three tools (`roll_dice`, `get_weather`, `get_date`) work correctly - [ ] Input validation prevents crashes and provides helpful error messages - [ ] Weather API integration handles rate limits and failures gracefully - [ ] Timezone support works correctly for date/time requests - [ ] All code passes linting (ruff) and type checking (mypy) - [ ] Comprehensive test coverage for all tools and edge cases - [ ] Server follows project conventions and patterns ## All Needed Context ### Documentation & References ```yaml # MUST READ - Include these in your context window - url: https://github.com/modelcontextprotocol/python-sdk why: Official MCP Python SDK for protocol implementation - url: https://github.com/jlowin/fastmcp why: FastMCP framework for simplified MCP server development - url: https://open-meteo.com/en/docs why: Weather API documentation for get_weather tool section: Current weather conditions and forecast endpoints critical: No API key required for basic usage, supports lat/lon coordinates - url: https://docs.python.org/3/library/datetime.html why: Timezone handling and ISO 8601 formatting for get_date tool section: timezone-aware datetime objects and isoformat() critical: Use timezone.utc for UTC times, avoid naive datetimes - url: https://docs.python.org/3/library/random.html why: Random number generation for dice rolling section: randint() for generating dice values critical: Use random.randint(1, sides) for each die roll - file: /workspaces/context-engineering-template/context/examples/features/feature_1_mcp_server.md why: Original feature requirements with examples and considerations - file: /workspaces/context-engineering-template/pyproject.toml why: Project configuration, dependencies, and tool settings - file: /workspaces/context-engineering-template/src/main.py why: Current main entry point pattern to follow ``` ### Current Codebase tree ```bash /workspaces/context-engineering-template ├── AGENTS.md ├── assets/ ├── CHANGELOG.md ├── CLAUDE.md ├── context/ │ ├── examples/ │ │ └── features/ │ │ └── feature_1_mcp_server.md │ └── templates/ ├── docs/ ├── LICENSE ├── Makefile ├── mkdocs.yaml ├── pyproject.toml ├── README.md ├── src/ │ ├── __init__.py │ ├── main.py │ └── py.typed └── uv.lock ``` ### Desired Codebase tree with files to be added ```bash /workspaces/context-engineering-template ├── src/ │ ├── __init__.py │ ├── main.py # Modified to support MCP server │ ├── py.typed │ ├── mcp_server/ │ │ ├── __init__.py │ │ ├── server.py # Main MCP server implementation │ │ ├── tools/ │ │ │ ├── __init__.py │ │ │ ├── base.py # Base tool interface │ │ │ ├── dice.py # Roll dice tool │ │ │ ├── weather.py # Weather API tool │ │ │ └── date_time.py # Date/time tool │ │ └── models/ │ │ ├── __init__.py │ │ └── requests.py # Pydantic models for requests/responses ├── tests/ │ ├── __init__.py │ ├── test_mcp_server.py │ ├── test_tools/ │ │ ├── __init__.py │ │ ├── test_dice.py │ │ ├── test_weather.py │ │ └── test_date_time.py │ └── fixtures/ │ ├── __init__.py │ └── mcp_messages.py # Test message fixtures ├── context/ │ └── examples/outputs │ ├── roll_dice_example.json # Example request/response │ ├── get_weather_example.json # Example request/response │ └── get_date_example.json # Example request/response ``` ### Known Gotchas & Library Quirks ```python # CRITICAL: MCP protocol requires specific message structure # Must follow exact JSON-RPC 2.0 format with method/params/id fields # CRITICAL: FastMCP requires async functions for tool definitions # Example: @mcp.tool() async def roll_dice(notation: str) -> dict: # Implementation # CRITICAL: Open-Meteo API requires lat/lon coordinates, not city names # Must use geocoding service or coordinate lookup for city names # API endpoint: https://api.open-meteo.com/v1/forecast?latitude=X&longitude=Y # CRITICAL: Python datetime timezone handling # Always use timezone-aware datetime objects # Use datetime.now(timezone.utc) for UTC times # Use zoneinfo (Python 3.9+) or pytz for timezone conversions # CRITICAL: Dice notation validation # Valid formats: "1d6", "2d10", "3d20" (number + 'd' + sides) # Invalid: "d6", "1x6", "0d6", "1d0", "abc" # Must validate both dice count and sides > 0 # CRITICAL: Error handling should not expose internal details # Return user-friendly error messages without stack traces # Log detailed errors internally for debugging ``` ## Implementation Blueprint ### Data models and structure Create core data models for type safety and validation: ```python # src/mcp_server/models/requests.py from pydantic import BaseModel, Field, validator from typing import Optional, Union, Any class MCPRequest(BaseModel): """Base MCP request structure""" jsonrpc: str = "2.0" method: str params: Optional[dict] = None id: Optional[Union[str, int]] = None class MCPResponse(BaseModel): """Base MCP response structure""" jsonrpc: str = "2.0" id: Optional[Union[str, int]] = None result: Optional[Any] = None error: Optional[dict] = None class DiceRollRequest(BaseModel): """Dice roll tool request""" notation: str = Field(..., description="Dice notation like '2d6' or '1d20'") @validator('notation') def validate_notation(cls, v): # Validate dice notation format pass class WeatherRequest(BaseModel): """Weather tool request""" location: str = Field(..., description="City name or coordinates") class DateTimeRequest(BaseModel): """Date/time tool request""" timezone: Optional[str] = Field("UTC", description="Timezone identifier") ``` ### List of tasks to be completed in order ```yaml Task 1: Setup Dependencies ADD to pyproject.toml: - fastmcp>=2.0.0 (or mcp package if using official SDK) - httpx (for weather API calls) - pydantic>=2.0.0 (for data validation) - python-dateutil (for timezone handling) Task 2: Create Base Tool Interface CREATE src/mcp_server/tools/base.py: - DEFINE abstract base class for all tools - ESTABLISH common interface for tool registration - INCLUDE error handling patterns Task 3: Implement Dice Rolling Tool CREATE src/mcp_server/tools/dice.py: - IMPLEMENT dice notation parsing (regex: r'(\d+)d(\d+)') - VALIDATE dice count > 0 and sides > 0 - GENERATE random values using random.randint(1, sides) - RETURN structured result with individual values and total Task 4: Implement Weather Tool CREATE src/mcp_server/tools/weather.py: - IMPLEMENT city name to coordinates lookup (basic hardcoded mapping) - INTEGRATE with Open-Meteo API using httpx - HANDLE API failures and rate limits gracefully - RETURN structured weather data (temperature, condition, wind) Task 5: Implement Date/Time Tool CREATE src/mcp_server/tools/date_time.py: - IMPLEMENT timezone parsing using zoneinfo - GENERATE current datetime for requested timezone - FORMAT output as ISO 8601 string - HANDLE invalid timezone names gracefully Task 6: Create MCP Server CREATE src/mcp_server/server.py: - IMPLEMENT FastMCP server with tool registration - SETUP message routing and error handling - INTEGRATE all three tools with proper decorators - HANDLE protocol-level errors and validation Task 7: Update Main Entry Point MODIFY src/main.py: - IMPORT MCP server module - SETUP server initialization and startup - HANDLE graceful shutdown - PRESERVE existing async pattern Task 8: Create Example Files CREATE context/examples/outputs/*.json: - GENERATE realistic request/response examples - INCLUDE both success and error cases - FOLLOW exact JSON structure from feature requirements Task 9: Implement Comprehensive Tests CREATE tests/test_*.py: - UNIT tests for each tool with edge cases - INTEGRATION tests for MCP server - MOCK external API calls for weather tool - VALIDATE error handling and edge cases Task 10: Final Integration and Validation RUN validation commands: - EXECUTE make ruff (code formatting) - EXECUTE make type_check (mypy validation) - EXECUTE make test_all (pytest suite) - MANUAL test server startup and tool execution ``` ### Per task pseudocode ```python # Task 3: Dice Rolling Tool class DiceRoller: def __init__(self): self.notation_pattern = re.compile(r'^(\d+)d(\d+)$') async def roll_dice(self, notation: str) -> dict: # PATTERN: Always validate input first match = self.notation_pattern.match(notation.lower()) if not match: raise ValueError(f"Invalid dice notation: {notation}") dice_count, sides = int(match.group(1)), int(match.group(2)) # GOTCHA: Validate reasonable limits if dice_count <= 0 or dice_count > 100: raise ValueError("Dice count must be 1-100") if sides <= 0 or sides > 1000: raise ValueError("Sides must be 1-1000") # PATTERN: Generate results values = [random.randint(1, sides) for _ in range(dice_count)] return { "values": values, "total": sum(values), "notation": notation } # Task 4: Weather Tool class WeatherTool: def __init__(self): self.api_base = "https://api.open-meteo.com/v1" self.city_coords = { "san francisco": (37.7749, -122.4194), "new york": (40.7128, -74.0060), # Add more as needed } async def get_weather(self, location: str) -> dict: # PATTERN: Input validation and preprocessing location_lower = location.lower() # GOTCHA: Handle coordinate lookup if location_lower in self.city_coords: lat, lon = self.city_coords[location_lower] else: # Try to parse as "lat,lon" format try: lat, lon = map(float, location.split(',')) except ValueError: raise ValueError(f"Unknown location: {location}") # CRITICAL: API call with proper error handling async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_base}/forecast", params={ "latitude": lat, "longitude": lon, "current": "temperature_2m,weather_code,wind_speed_10m" }, timeout=10.0 ) response.raise_for_status() data = response.json() return { "location": location, "temperature": data["current"]["temperature_2m"], "condition": self._weather_code_to_text(data["current"]["weather_code"]), "wind_speed": data["current"]["wind_speed_10m"] } except httpx.TimeoutException: raise Exception("Weather service timeout") except httpx.HTTPStatusError as e: raise Exception(f"Weather service error: {e.response.status_code}") # Task 5: Date/Time Tool class DateTimeTool: async def get_date(self, timezone: str = "UTC") -> str: # PATTERN: Timezone validation try: if timezone.upper() == "UTC": tz = timezone.utc else: tz = zoneinfo.ZoneInfo(timezone) except zoneinfo.ZoneInfoNotFoundError: raise ValueError(f"Invalid timezone: {timezone}") # PATTERN: Generate timezone-aware datetime current_time = datetime.now(tz) # CRITICAL: Return ISO 8601 format return current_time.isoformat() # Task 6: MCP Server Integration from fastmcp import FastMCP mcp = FastMCP("dice-weather-datetime-server") @mcp.tool() async def roll_dice(notation: str) -> dict: """Roll dice using standard notation like '2d6' or '1d20'""" return await dice_tool.roll_dice(notation) @mcp.tool() async def get_weather(location: str) -> dict: """Get current weather for a location""" return await weather_tool.get_weather(location) @mcp.tool() async def get_date(timezone: str = "UTC") -> str: """Get current date and time for a timezone""" return await datetime_tool.get_date(timezone) ``` ### Integration Points ```yaml DEPENDENCIES: - add to: pyproject.toml - packages: ["fastmcp>=2.0.0", "httpx", "pydantic>=2.0.0", "python-dateutil"] MAIN_ENTRY: - modify: src/main.py - pattern: "async def main() -> None:" - add server startup and stdio transport setup TESTING: - add to: tests/ - pattern: "pytest fixtures for mocking external APIs" - mock httpx responses for weather API ``` ## Validation Loop ### Level 1: Syntax & Style ```bash # Run these FIRST - fix any errors before proceeding uv run ruff check src/ --fix uv run mypy src/ # Expected: No errors. If errors exist, read and fix them. ``` ### Level 2: Unit Tests ```python # CREATE test_dice.py def test_roll_dice_valid_notation(): """Test valid dice notation works""" result = await dice_tool.roll_dice("2d6") assert len(result["values"]) == 2 assert all(1 <= v <= 6 for v in result["values"]) assert result["total"] == sum(result["values"]) def test_roll_dice_invalid_notation(): """Test invalid notation raises ValueError""" with pytest.raises(ValueError, match="Invalid dice notation"): await dice_tool.roll_dice("invalid") def test_get_weather_known_city(): """Test weather for known city""" with httpx_mock.HTTPXMock() as m: m.add_response(json={"current": {"temperature_2m": 20, "weather_code": 0, "wind_speed_10m": 5}}) result = await weather_tool.get_weather("San Francisco") assert result["temperature"] == 20 def test_get_date_utc(): """Test UTC date retrieval""" result = await datetime_tool.get_date("UTC") assert result.endswith("Z") # UTC timezone indicator ``` ```bash # Run and iterate until passing: uv run pytest tests/ -v # If failing: Read error, fix code, re-run ``` ### Level 3: Integration Test ```bash # Test MCP server startup uv run python -m src.main # Test tool execution (manual verification) # Send JSON-RPC message to stdin and verify response format echo '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "roll_dice", "arguments": {"notation": "2d6"}}, "id": 1}' | uv run python -m src.main ``` ## Final validation Checklist - [ ] All tests pass: `uv run pytest tests/ -v` - [ ] No linting errors: `uv run ruff check src/` - [ ] No type errors: `uv run mypy src/` - [ ] MCP server starts without errors - [ ] All three tools respond correctly to valid inputs - [ ] Invalid inputs return helpful error messages - [ ] Weather API failures are handled gracefully - [ ] Timezone handling works for common timezones - [ ] Example JSON files are valid and match implementation ## Anti-Patterns to Avoid - ❌ Don't use synchronous functions in async context - ❌ Don't ignore MCP protocol message structure requirements - ❌ Don't expose internal error details to users - ❌ Don't hardcode API endpoints or configuration values - ❌ Don't skip input validation because "it should work" - ❌ Don't use naive datetime objects for timezone-aware operations - ❌ Don't catch all exceptions - be specific about error handling - ❌ Don't mock away all external dependencies in tests - verify integration points --- ## Quality Score: 8/10 This PRP provides comprehensive context for one-pass implementation: - ✅ Complete MCP protocol documentation and examples - ✅ Detailed implementation patterns and gotchas - ✅ Specific API documentation and error handling strategies - ✅ Executable validation commands and test patterns - ✅ Clear task breakdown with pseudocode - ✅ Anti-patterns and common pitfalls identified **Areas for improvement:** - Could include more city coordinate mappings for weather tool - Could provide more specific timezone examples for testing **Confidence Level:** High - This PRP contains sufficient technical detail and context for successful implementation by an AI agent with access to the codebase and web search capabilities. ================================================ FILE: examples/mcp-server-client/context/PRPs/feature_2_mcp_client.md ================================================ # MCP Client Implementation for Tool Invocation ## Purpose Product Requirements Prompt (PRP) for implementing a complete MCP (Model Context Protocol) client in Python that can connect to MCP servers and invoke tools. This PRP provides comprehensive context for one-pass implementation success. ## Goal Build a production-ready MCP client in Python that: - Implements the MCP protocol for structured message handling - Connects to MCP servers via stdio transport - Provides CLI interface for invoking tools: `roll_dice`, `get_weather`, and `get_date` - Handles connection errors, timeouts, and retries gracefully - Follows the project's established patterns and conventions - Is modular, testable, and extensible for future tools ## Why - **Standardization**: Implements the MCP protocol standard for AI tool integration - **Modularity**: Creates a foundation for connecting to any MCP server - **User Value**: Provides command-line access to remote tools via structured protocol - **Learning**: Demonstrates best practices for building MCP clients in Python - **Complement**: Works with the existing MCP server implementation in the codebase ## What ### User-visible behavior - Client connects to MCP server via stdio transport - CLI accepts tool invocation commands like `mcp-client roll_dice --notation 3d6` - Returns structured responses with results or detailed error messages - Handles server connection failures gracefully with informative feedback - Supports interactive mode for multiple tool invocations ### Success Criteria - [ ] MCP client connects to local MCP server via stdio transport - [ ] All three tools (`roll_dice`, `get_weather`, `get_date`) can be invoked via CLI - [ ] Connection error handling provides helpful user feedback - [ ] Tool invocation validation prevents malformed requests - [ ] JSON-RPC 2.0 protocol compliance for all messages - [ ] All code passes linting (ruff) and type checking (mypy) - [ ] Comprehensive test coverage for all client functionality - [ ] Client follows project conventions and patterns ## All Needed Context ### Documentation & References ```yaml # MUST READ - Include these in your context window - url: https://github.com/modelcontextprotocol/python-sdk why: Official MCP Python SDK for protocol implementation section: Client implementation patterns and transport methods critical: Use mcp.ClientSession and StdioServerParameters for connections - url: https://modelcontextprotocol.io/quickstart/client why: Official MCP client quickstart guide section: Client connection patterns and message handling critical: AsyncExitStack for resource management, proper session handling - url: https://docs.python.org/3/library/argparse.html why: CLI argument parsing for tool invocation interface section: Subcommands and argument validation critical: Use subparsers for different tool commands - url: https://docs.python.org/3/library/asyncio.html why: Asynchronous programming patterns for MCP client section: async/await patterns and event loop management critical: Use asyncio.run() for main entry point - url: https://docs.python.org/3/library/json.html why: JSON message serialization/deserialization section: JSON encoding/decoding with proper error handling critical: Handle malformed JSON responses gracefully - file: /workspaces/context-engineering-template/src/mcp_server/ why: Complete MCP server implementation to connect to section: server.py shows the exact tool interface and message format critical: Tools available: roll_dice, get_weather, get_date with specific argument formats - file: /workspaces/context-engineering-template/src/mcp_server/models/requests.py why: Pydantic models for request/response validation section: MCPRequest, MCPResponse, and tool-specific models critical: Exact JSON-RPC 2.0 message structure and validation rules - file: /workspaces/context-engineering-template/context/examples/outputs why: Example request/response patterns for all tools section: roll_dice_example.json, get_weather_example.json, get_date_example.json critical: Exact JSON-RPC message format with method "tools/call" - file: /workspaces/context-engineering-template/pyproject.toml why: Project configuration, dependencies, and tool settings section: Dependencies and development tool configuration critical: Must use existing dependency management patterns ``` ### Current Codebase Structure ```bash /workspaces/context-engineering-template ├── AGENTS.md # Agent behavior guidelines ├── CLAUDE.md # Project instructions ├── pyproject.toml # Python project configuration ├── src/ │ ├── __init__.py │ ├── main.py # Main entry point (will be modified) │ ├── py.typed │ └── mcp_server/ # Existing MCP server implementation │ ├── __init__.py │ ├── server.py # MCP server with 3 tools │ ├── models/ │ │ ├── __init__.py │ │ └── requests.py # Pydantic models for validation │ └── tools/ │ ├── __init__.py │ ├── base.py # Base tool interface │ ├── dice.py # roll_dice implementation │ ├── weather.py # get_weather implementation │ └── date_time.py # get_date implementation ├── tests/ │ ├── __init__.py │ └── test_mcp_server.py # Existing server tests └── context/ └── examples/outputs ├── roll_dice_example.json # Tool invocation examples ├── get_weather_example.json # Tool invocation examples └── get_date_example.json # Tool invocation examples ``` ### Desired Codebase Structure with Files to be Added ```bash /workspaces/context-engineering-template ├── src/ │ ├── __init__.py │ ├── main.py # Modified to support MCP client CLI │ ├── py.typed │ ├── mcp_server/ # Existing (unchanged) │ └── mcp_client/ # NEW - MCP client implementation │ ├── __init__.py │ ├── client.py # Main MCP client class │ ├── cli.py # CLI interface for tool invocation │ ├── transport.py # Connection and transport handling │ └── models/ │ ├── __init__.py │ └── responses.py # Client-specific response models ├── tests/ │ ├── __init__.py │ ├── test_mcp_server.py # Existing (unchanged) │ ├── test_mcp_client.py # NEW - Client tests │ └── test_cli.py # NEW - CLI interface tests └── context/ └── examples/outputs ├── client_roll_dice_input.json # Simple input format ├── client_get_weather_input.json # Simple input format ├── client_get_date_input.json # Simple input format └── client_invalid_tool.json # Error handling example ``` ### Known Gotchas & Library Quirks ```python # CRITICAL: MCP protocol requires exact JSON-RPC 2.0 message structure # Client messages must include: jsonrpc, method, params, id # Example: { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "roll_dice", "arguments": {"notation": "2d6"} }, "id": 1 } # CRITICAL: MCP Python SDK client connection pattern # Must use AsyncExitStack for proper resource management async with AsyncExitStack() as stack: session = await stack.enter_async_context( stdio_client(StdioServerParameters( command="python", args=[server_script_path] )) ) # CRITICAL: Tool invocation through MCP client session # Use session.call_tool() method with exact tool name and arguments result = await session.call_tool("roll_dice", {"notation": "2d6"}) # CRITICAL: Server connection validation # Must check if server is running and tools are available tools = await session.list_tools() if not any(tool.name == "roll_dice" for tool in tools.tools): raise ValueError("roll_dice tool not available") # CRITICAL: CLI argument parsing for tool commands # Use subparsers for different tool commands parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='tool', help='Available tools') dice_parser = subparsers.add_parser('roll_dice', help='Roll dice') dice_parser.add_argument('--notation', required=True, help='Dice notation (e.g., 2d6)') # CRITICAL: Error handling for connection failures # Distinguish between connection errors and tool execution errors try: result = await session.call_tool(tool_name, arguments) except Exception as e: if "connection" in str(e).lower(): print(f"Connection error: {e}") else: print(f"Tool execution error: {e}") # CRITICAL: JSON serialization for complex tool arguments # Some tools accept complex arguments that need proper JSON handling import json arguments = json.loads(args.arguments) if args.arguments else {} ``` ## Implementation Blueprint ### Data Models and Structure Create client-specific models for type safety and validation: ```python # src/mcp_client/models/responses.py from pydantic import BaseModel, Field from typing import Optional, Union, Any, List class MCPToolResponse(BaseModel): """Response from MCP tool execution""" content: List[dict] = Field(..., description="Response content") isError: bool = Field(False, description="Whether response indicates error") class ClientToolResult(BaseModel): """Processed tool result for client consumption""" success: bool result: Optional[Any] = None error: Optional[str] = None tool_name: str arguments: dict class ClientSession(BaseModel): """Client session information""" server_path: str connected: bool = False available_tools: List[str] = Field(default_factory=list) ``` ### List of Tasks to be Completed in Order ```yaml Task 1: Setup Dependencies ADD to pyproject.toml: - mcp>=0.7.0 (official MCP Python SDK) - Use existing httpx, pydantic, and argparse dependencies Task 2: Create MCP Client Core CREATE src/mcp_client/client.py: - DEFINE MCPClient class with connection management - IMPLEMENT server connection via stdio transport - HANDLE tool discovery and validation - ESTABLISH session lifecycle management Task 3: Create Transport Layer CREATE src/mcp_client/transport.py: - IMPLEMENT stdio transport connection - HANDLE connection errors and retries - MANAGE server process lifecycle - PROVIDE connection health checks Task 4: Create CLI Interface CREATE src/mcp_client/cli.py: - IMPLEMENT argument parsing for tool commands - CREATE subcommands for each tool (roll_dice, get_weather, get_date) - HANDLE tool argument validation - PROVIDE user-friendly error messages Task 5: Integrate with Main Entry Point MODIFY src/main.py: - ADD MCP client CLI commands - PRESERVE existing functionality - HANDLE both client and server modes - MAINTAIN async pattern Task 6: Create Simple Input Examples CREATE context/examples/outputs/client_*.json: - GENERATE simple input format examples - INCLUDE error case example (invalid tool) - FOLLOW format from feature specification - ENABLE automated testing with these examples Task 7: Implement Response Processing CREATE src/mcp_client/models/responses.py: - DEFINE response processing models - HANDLE successful tool responses - PROCESS error responses with user-friendly messages - VALIDATE response format Task 8: Implement Comprehensive Tests CREATE tests/test_mcp_client.py and tests/test_cli.py: - UNIT tests for client connection and tool invocation - INTEGRATION tests with actual server - MOCK tests for connection failures - CLI command tests with argument validation Task 9: Add Error Handling and Retry Logic ENHANCE client.py and transport.py: - IMPLEMENT connection retry with exponential backoff - HANDLE server startup timeouts - PROVIDE graceful degradation for tool failures - LOG errors appropriately for debugging Task 10: Final Integration and Validation RUN validation commands: - EXECUTE make ruff (code formatting) - EXECUTE make type_check (mypy validation) - EXECUTE make test_all (pytest suite) - MANUAL test client-server integration ``` ### Per Task Pseudocode ```python # Task 2: MCP Client Core class MCPClient: def __init__(self, server_path: str): self.server_path = server_path self.session: Optional[ClientSession] = None self.available_tools: List[str] = [] async def connect(self) -> None: """Connect to MCP server via stdio""" # PATTERN: Use AsyncExitStack for resource management self.stack = AsyncExitStack() # CRITICAL: Validate server script exists if not os.path.exists(self.server_path): raise FileNotFoundError(f"Server script not found: {self.server_path}") # PATTERN: Create stdio connection server_params = StdioServerParameters( command="python" if self.server_path.endswith('.py') else "node", args=[self.server_path] ) try: self.session = await self.stack.enter_async_context( stdio_client(server_params) ) # CRITICAL: Discover available tools tools_result = await self.session.list_tools() self.available_tools = [tool.name for tool in tools_result.tools] except Exception as e: await self.stack.aclose() raise ConnectionError(f"Failed to connect to server: {e}") async def invoke_tool(self, tool_name: str, arguments: dict) -> ClientToolResult: """Invoke a tool on the connected server""" # PATTERN: Validate tool availability if tool_name not in self.available_tools: return ClientToolResult( success=False, error=f"Tool '{tool_name}' not available. Available: {self.available_tools}", tool_name=tool_name, arguments=arguments ) try: # CRITICAL: Use session.call_tool() for invocation result = await self.session.call_tool(tool_name, arguments) return ClientToolResult( success=True, result=result.content, tool_name=tool_name, arguments=arguments ) except Exception as e: return ClientToolResult( success=False, error=str(e), tool_name=tool_name, arguments=arguments ) async def disconnect(self) -> None: """Disconnect from server""" if self.stack: await self.stack.aclose() # Task 4: CLI Interface class MCPClientCLI: def __init__(self): self.parser = self._create_parser() self.client: Optional[MCPClient] = None def _create_parser(self) -> argparse.ArgumentParser: """Create argument parser with subcommands""" parser = argparse.ArgumentParser( description="MCP Client for tool invocation" ) parser.add_argument( '--server', required=True, help='Path to MCP server script' ) # PATTERN: Subcommands for each tool subparsers = parser.add_subparsers(dest='tool', help='Available tools') # Dice rolling tool dice_parser = subparsers.add_parser('roll_dice', help='Roll dice') dice_parser.add_argument( '--notation', required=True, help='Dice notation (e.g., 2d6, 1d20)' ) # Weather tool weather_parser = subparsers.add_parser('get_weather', help='Get weather') weather_parser.add_argument( '--location', required=True, help='Location name or coordinates' ) # Date tool date_parser = subparsers.add_parser('get_date', help='Get date/time') date_parser.add_argument( '--timezone', default='UTC', help='Timezone (default: UTC)' ) return parser async def run(self, args: List[str]) -> None: """Run CLI with provided arguments""" parsed_args = self.parser.parse_args(args) if not parsed_args.tool: self.parser.print_help() return # PATTERN: Connect to server self.client = MCPClient(parsed_args.server) try: await self.client.connect() # PATTERN: Build tool arguments tool_args = {} if parsed_args.tool == 'roll_dice': tool_args = {'notation': parsed_args.notation} elif parsed_args.tool == 'get_weather': tool_args = {'location': parsed_args.location} elif parsed_args.tool == 'get_date': tool_args = {'timezone': parsed_args.timezone} # PATTERN: Invoke tool and display result result = await self.client.invoke_tool(parsed_args.tool, tool_args) if result.success: self._display_success(result) else: self._display_error(result) except Exception as e: print(f"Error: {e}") finally: if self.client: await self.client.disconnect() def _display_success(self, result: ClientToolResult) -> None: """Display successful tool result""" print(f"✅ {result.tool_name} executed successfully:") # PATTERN: Handle different response formats if result.result and isinstance(result.result, list): for item in result.result: if isinstance(item, dict) and 'text' in item: print(item['text']) else: print(json.dumps(item, indent=2)) else: print(json.dumps(result.result, indent=2)) def _display_error(self, result: ClientToolResult) -> None: """Display tool execution error""" print(f"❌ {result.tool_name} failed:") print(f"Error: {result.error}") ``` ### Integration Points ```yaml DEPENDENCIES: - add to: pyproject.toml - packages: ["mcp>=0.7.0"] - note: Use existing httpx, pydantic, argparse dependencies MAIN_ENTRY: - modify: src/main.py - pattern: "async def main() -> None:" - add client CLI command handling alongside server functionality TESTING: - add to: tests/ - pattern: "pytest fixtures for client-server integration" - mock server responses for unit tests - integration tests with actual server startup ``` ## Validation Loop ### Level 1: Syntax & Style ```bash # Run these FIRST - fix any errors before proceeding uv run ruff check src/ --fix uv run mypy src/ # Expected: No errors. If errors exist, read and fix them. ``` ### Level 2: Unit Tests ```python # CREATE test_mcp_client.py async def test_client_connection(): """Test client can connect to server""" client = MCPClient("src/mcp_server/server.py") await client.connect() assert len(client.available_tools) == 3 assert "roll_dice" in client.available_tools await client.disconnect() async def test_tool_invocation(): """Test tool invocation works correctly""" client = MCPClient("src/mcp_server/server.py") await client.connect() result = await client.invoke_tool("roll_dice", {"notation": "2d6"}) assert result.success assert result.tool_name == "roll_dice" await client.disconnect() async def test_invalid_tool(): """Test invalid tool handling""" client = MCPClient("src/mcp_server/server.py") await client.connect() result = await client.invoke_tool("invalid_tool", {}) assert not result.success assert "not available" in result.error await client.disconnect() def test_cli_argument_parsing(): """Test CLI argument parsing""" cli = MCPClientCLI() # Test dice command args = ['--server', 'server.py', 'roll_dice', '--notation', '2d6'] parsed = cli.parser.parse_args(args) assert parsed.tool == 'roll_dice' assert parsed.notation == '2d6' # Test weather command args = ['--server', 'server.py', 'get_weather', '--location', 'London'] parsed = cli.parser.parse_args(args) assert parsed.tool == 'get_weather' assert parsed.location == 'London' ``` ```bash # Run and iterate until passing: uv run pytest tests/test_mcp_client.py -v uv run pytest tests/test_cli.py -v # If failing: Read error, fix code, re-run ``` ### Level 3: Integration Test ```bash # Test client-server integration # 1. Start server in background uv run python -m src.mcp_server.server & # 2. Test client connection and tool invocation uv run python -m src.main --server src/mcp_server/server.py roll_dice --notation 2d6 uv run python -m src.main --server src/mcp_server/server.py get_weather --location "San Francisco" uv run python -m src.main --server src/mcp_server/server.py get_date --timezone UTC # 3. Test error handling uv run python -m src.main --server src/mcp_server/server.py roll_dice --notation invalid ``` ## Final Validation Checklist - [ ] All tests pass: `uv run pytest tests/ -v` - [ ] No linting errors: `uv run ruff check src/` - [ ] No type errors: `uv run mypy src/` - [ ] Client connects to server successfully - [ ] All three tools can be invoked via CLI - [ ] Invalid tool names return helpful error messages - [ ] Invalid tool arguments return helpful error messages - [ ] Connection errors are handled gracefully - [ ] Simple input JSON files are created and valid - [ ] CLI help messages are clear and informative ## Anti-Patterns to Avoid - ❌ Don't use synchronous functions in async context - ❌ Don't ignore MCP protocol message structure requirements - ❌ Don't expose internal connection details to CLI users - ❌ Don't hardcode server paths or tool names - ❌ Don't skip connection validation before tool invocation - ❌ Don't catch all exceptions - be specific about error types - ❌ Don't assume server is always available - handle startup failures - ❌ Don't block the event loop with synchronous operations - ❌ Don't ignore resource cleanup - always close connections --- ## Quality Score: 9/10 This PRP provides comprehensive context for one-pass implementation: - ✅ Complete MCP protocol documentation and client patterns - ✅ Detailed existing server implementation to connect to - ✅ Specific CLI interface requirements and argument parsing - ✅ Executable validation commands and test patterns - ✅ Clear task breakdown with pseudocode and examples - ✅ Anti-patterns and common pitfalls identified - ✅ Integration points with existing codebase clearly defined - ✅ Example files structure and format specifications **Areas for improvement:** - Could include more specific error message examples - Could provide more detailed transport layer specifications **Confidence Level:** Very High - This PRP contains comprehensive technical detail, existing implementation context, and clear validation steps for successful implementation by an AI agent with access to the codebase and web search capabilities. ================================================ FILE: examples/mcp-server-client/context/PRPs/feature_3_streamlit_gui.md ================================================ # Streamlit GUI for MCP Server-Client Interaction Showcase ## Purpose Product Requirements Prompt (PRP) for implementing a comprehensive Streamlit-based GUI that demonstrates MCP Server-Client interaction. This PRP provides complete context for building an interactive interface that showcases tool invocation, real-time communication, and user-friendly error handling. ## Goal Build a production-ready Streamlit GUI that: - Provides interactive forms for invoking MCP tools (`roll_dice`, `get_weather`, `get_date`) - Displays real-time request/response payloads with proper formatting - Shows connection status and handles errors gracefully - Maintains interaction history for the current session - Serves as both a testing interface and demonstration tool - Follows the project's established patterns and conventions - Is responsive, user-friendly, and extensible for future tools ## Why - **User Experience**: Provides non-technical users access to MCP tools without CLI - **Demonstration**: Visual showcase of MCP protocol communication patterns - **Testing**: Interactive interface for manual testing and validation - **Learning**: Demonstrates best practices for building Streamlit applications - **Integration**: Showcases seamless integration between GUI and backend services - **Accessibility**: Makes MCP tools accessible to users unfamiliar with command-line interfaces ## What ### User-visible behavior - Interactive web interface accessible via browser - Form inputs for each tool with validation and help text - Real-time display of JSON request/response payloads - Connection status indicator with health checks - Session history showing previous interactions - Error messages displayed with helpful context - Visual feedback for request processing states ### Success Criteria - [ ] Streamlit GUI launches and displays tool selection interface - [ ] All three tools can be invoked through interactive forms - [ ] Request/response payloads are displayed in real-time - [ ] Connection status is clearly indicated and updated - [ ] Error handling provides user-friendly messages - [ ] Session history tracks all interactions - [ ] GUI is responsive and provides good user experience - [ ] All code passes linting (ruff) and type checking (mypy) - [ ] GUI integration tests validate core functionality ## All Needed Context ### Documentation & References ```yaml # MUST READ - Include these in your context window - url: https://docs.streamlit.io/ why: Official Streamlit documentation for building interactive web apps section: Session state, forms, columns, and real-time updates critical: Use st.session_state for maintaining connection and history - url: https://docs.streamlit.io/develop/concepts/architecture/session-state why: Session state management for maintaining GUI state section: Session state patterns and best practices critical: Persistent connection state across user interactions - url: https://docs.streamlit.io/develop/api-reference/widgets/form why: Form widgets for tool parameter input section: Form validation and submission handling critical: Use st.form() for batch input submission - url: https://docs.streamlit.io/develop/api-reference/status why: Status indicators and progress feedback section: st.spinner, st.success, st.error, st.warning critical: Real-time feedback for async operations - url: https://docs.streamlit.io/develop/api-reference/layout why: Layout components for organized GUI structure section: Columns, containers, and tabs for organized interface critical: Use st.columns() for side-by-side layout - file: /workspaces/context-engineering-template/src/mcp_client/ why: Complete MCP client implementation to integrate with GUI section: client.py, transport.py, and models/responses.py critical: Use existing MCPClient class for server communication - file: /workspaces/context-engineering-template/src/mcp_server/ why: Complete MCP server implementation and tool specifications section: server.py and tools/ directory for tool interfaces critical: Exact tool parameters and response formats - file: /workspaces/context-engineering-template/context/examples/outputs why: Example request/response patterns and expected formats section: client_*.json files for input validation patterns critical: GUI input validation should match CLI patterns - file: /workspaces/context-engineering-template/pyproject.toml why: Project dependencies and configuration section: Current dependencies and development tools critical: Add streamlit to dependencies while maintaining existing patterns ``` ### Current Codebase Structure ```bash /workspaces/context-engineering-template ├── AGENTS.md # Agent behavior guidelines ├── CLAUDE.md # Project instructions ├── Makefile # Build and development commands ├── pyproject.toml # Python project configuration ├── src/ │ ├── __init__.py │ ├── main.py # Main entry point (will be modified) │ ├── py.typed │ ├── mcp_client/ # Complete MCP client implementation │ │ ├── __init__.py │ │ ├── client.py # MCPClient class for server communication │ │ ├── cli.py # CLI interface (patterns to follow) │ │ ├── transport.py # Connection and transport handling │ │ └── models/ │ │ ├── __init__.py │ │ └── responses.py # Client response models │ └── mcp_server/ # Complete MCP server implementation │ ├── __init__.py │ ├── server.py # MCP server with 3 tools │ ├── models/ │ │ ├── __init__.py │ │ └── requests.py # Tool request/response models │ └── tools/ │ ├── __init__.py │ ├── base.py # Base tool interface │ ├── dice.py # roll_dice implementation │ ├── weather.py # get_weather implementation │ └── date_time.py # get_date implementation ├── tests/ │ ├── __init__.py │ ├── test_mcp_client.py # Client tests (patterns to follow) │ ├── test_mcp_server.py # Server tests │ └── test_cli.py # CLI tests (patterns to follow) └── context/ └── examples/outputs ├── client_roll_dice_input.json # Input format examples ├── client_get_weather_input.json # Input format examples ├── client_get_date_input.json # Input format examples └── client_invalid_tool.json # Error handling examples ``` ### Desired Codebase Structure with Files to be Added ```bash /workspaces/context-engineering-template ├── src/ │ ├── __init__.py │ ├── main.py # Modified to support GUI launch │ ├── py.typed │ ├── mcp_client/ # Existing (unchanged) │ ├── mcp_server/ # Existing (unchanged) │ └── gui/ # NEW - Streamlit GUI implementation │ ├── __init__.py │ ├── app.py # Main Streamlit application │ ├── components/ # Reusable GUI components │ │ ├── __init__.py │ │ ├── tool_forms.py # Tool-specific form components │ │ ├── connection.py # Connection status components │ │ └── history.py # Session history components │ ├── utils/ │ │ ├── __init__.py │ │ ├── formatting.py # JSON formatting and display utilities │ │ └── validation.py # Input validation utilities │ └── models/ │ ├── __init__.py │ └── gui_models.py # GUI-specific data models ├── tests/ │ ├── __init__.py │ ├── test_mcp_client.py # Existing (unchanged) │ ├── test_mcp_server.py # Existing (unchanged) │ ├── test_cli.py # Existing (unchanged) │ └── test_gui.py # NEW - GUI component tests └── context/ └── examples/outputs ├── streamlit_roll_dice_interaction.json # GUI interaction examples ├── streamlit_get_weather_interaction.json # GUI interaction examples ├── streamlit_get_date_interaction.json # GUI interaction examples └── streamlit_error_handling.json # GUI error examples ``` ### Known Gotchas & Library Quirks ```python # CRITICAL: Streamlit session state management # Must use st.session_state for persistent data across reruns if 'mcp_client' not in st.session_state: st.session_state.mcp_client = None st.session_state.connected = False st.session_state.interaction_history = [] # CRITICAL: Streamlit async operation handling # Streamlit doesn't natively support async/await - must use asyncio.run() import asyncio result = asyncio.run(client.invoke_tool(tool_name, arguments)) # CRITICAL: Streamlit form submission pattern # Use st.form() to batch inputs and prevent premature submission with st.form("tool_form"): param1 = st.text_input("Parameter 1") param2 = st.text_input("Parameter 2") submitted = st.form_submit_button("Submit") if submitted: # Process form data here pass # CRITICAL: Streamlit rerun behavior # GUI reruns on every interaction - must handle state carefully # Use st.rerun() to trigger manual updates after async operations if st.button("Connect"): asyncio.run(connect_to_server()) st.rerun() # Force refresh after connection # CRITICAL: MCP client connection management in GUI # Must handle connection lifecycle properly in Streamlit context async def ensure_connection(): if st.session_state.mcp_client is None: st.session_state.mcp_client = MCPClient("src/mcp_server/server.py") if not st.session_state.mcp_client.connected: await st.session_state.mcp_client.connect() st.session_state.connected = True # CRITICAL: JSON display formatting # Use st.json() for proper JSON display with syntax highlighting st.json({"request": request_data, "response": response_data}) # CRITICAL: Error handling in Streamlit # Use st.error() for error messages and st.exception() for stack traces try: result = await client.invoke_tool(tool_name, args) st.success("Tool executed successfully!") except Exception as e: st.error(f"Error: {str(e)}") st.exception(e) # Show full stack trace in development # CRITICAL: Real-time updates with spinner # Use st.spinner() for long-running operations with st.spinner("Executing tool..."): result = asyncio.run(client.invoke_tool(tool_name, args)) ``` ## Implementation Blueprint ### Data Models and Structure Create GUI-specific models for state management and type safety: ```python # src/gui/models/gui_models.py from pydantic import BaseModel, Field from typing import Optional, Any, List from datetime import datetime class GUIInteraction(BaseModel): """Single tool interaction record for GUI history""" timestamp: datetime = Field(default_factory=datetime.now) tool_name: str arguments: dict[str, Any] request_payload: dict[str, Any] response_payload: dict[str, Any] success: bool error_message: Optional[str] = None execution_time: Optional[float] = None class GUISession(BaseModel): """GUI session state management""" connected: bool = False server_path: str = "src/mcp_server/server.py" available_tools: List[str] = Field(default_factory=list) interaction_history: List[GUIInteraction] = Field(default_factory=list) current_tool: Optional[str] = None class ConnectionStatus(BaseModel): """Connection status information""" connected: bool = False server_path: str last_health_check: Optional[datetime] = None available_tools: List[str] = Field(default_factory=list) error_message: Optional[str] = None ``` ### List of Tasks to be Completed in Order ```yaml Task 1: Setup Dependencies ADD to pyproject.toml: - streamlit>=1.28.0 (latest stable version) - Use existing asyncio, json, and datetime from stdlib - PRESERVE existing dependency versions Task 2: Create Main GUI Application CREATE src/gui/app.py: - IMPLEMENT main Streamlit application entry point - SETUP session state management for persistent connection - CREATE main page layout with tool selection interface - ESTABLISH connection management and health checking Task 3: Create GUI Models CREATE src/gui/models/gui_models.py: - DEFINE GUIInteraction, GUISession, and ConnectionStatus models - IMPLEMENT Pydantic validation for GUI state - PROVIDE type safety for session management - ENABLE structured interaction history Task 4: Create Connection Management Component CREATE src/gui/components/connection.py: - IMPLEMENT connection status display with health indicator - HANDLE connection establishment and error recovery - PROVIDE real-time connection monitoring - DISPLAY available tools and server information Task 5: Create Tool Form Components CREATE src/gui/components/tool_forms.py: - IMPLEMENT dice roll form with notation validation - IMPLEMENT weather form with location input and examples - IMPLEMENT date/time form with timezone selection - PROVIDE input validation and helpful user guidance Task 6: Create History Management Component CREATE src/gui/components/history.py: - IMPLEMENT session history display with filtering - PROVIDE interaction timeline with timestamps - ENABLE request/response payload inspection - SUPPORT history export and clearing Task 7: Create Formatting and Validation Utilities CREATE src/gui/utils/formatting.py and validation.py: - IMPLEMENT JSON formatting for request/response display - PROVIDE syntax highlighting for JSON payloads - IMPLEMENT input validation with user-friendly messages - HANDLE error formatting and display Task 8: Integration with Main Entry Point MODIFY src/main.py: - ADD GUI launch command alongside existing CLI - PRESERVE existing server and client functionality - HANDLE GUI-specific argument parsing - MAINTAIN async pattern compatibility Task 9: Create Example Interaction Files CREATE context/examples/outputs/streamlit_*.json: - GENERATE example GUI interaction patterns - INCLUDE successful tool execution examples - PROVIDE error handling scenarios - ENABLE automated testing validation Task 10: Implement GUI Tests CREATE tests/test_gui.py: - UNIT tests for GUI components and utilities - INTEGRATION tests with MCP client functionality - MOCK tests for connection scenarios - STREAMLIT specific testing patterns Task 11: Add Make Commands MODIFY Makefile: - ADD run_gui command to launch Streamlit application - INTEGRATE GUI testing into existing test suite - PROVIDE GUI-specific development commands - MAINTAIN existing command structure ``` ### Per Task Pseudocode ```python # Task 2: Main GUI Application # src/gui/app.py import streamlit as st import asyncio from ..mcp_client.client import MCPClient from .models.gui_models import GUISession, GUIInteraction from .components.connection import ConnectionManager from .components.tool_forms import ToolForms from .components.history import HistoryManager def main(): """Main Streamlit application entry point""" st.set_page_config( page_title="MCP Tool Showcase", page_icon="🛠️", layout="wide", initial_sidebar_state="expanded" ) # PATTERN: Initialize session state if 'gui_session' not in st.session_state: st.session_state.gui_session = GUISession() if 'mcp_client' not in st.session_state: st.session_state.mcp_client = None # PATTERN: Main layout with sidebar with st.sidebar: st.title("🛠️ MCP Tool Showcase") connection_manager = ConnectionManager() connection_manager.render() # PATTERN: Main content area col1, col2 = st.columns([1, 1]) with col1: st.header("Tool Invocation") if st.session_state.gui_session.connected: tool_forms = ToolForms() tool_forms.render() else: st.info("Please connect to the MCP server first") with col2: st.header("Request/Response") if st.session_state.gui_session.interaction_history: latest_interaction = st.session_state.gui_session.interaction_history[-1] st.subheader("Latest Request") st.json(latest_interaction.request_payload) st.subheader("Latest Response") st.json(latest_interaction.response_payload) else: st.info("No interactions yet") # PATTERN: History section st.header("Interaction History") history_manager = HistoryManager() history_manager.render() # Task 4: Connection Management # src/gui/components/connection.py class ConnectionManager: """Manages MCP server connection in GUI""" def render(self): """Render connection management interface""" st.subheader("Connection Status") # PATTERN: Display current status if st.session_state.gui_session.connected: st.success("✅ Connected to MCP Server") st.write(f"Server: {st.session_state.gui_session.server_path}") st.write(f"Available Tools: {', '.join(st.session_state.gui_session.available_tools)}") # PATTERN: Health check button if st.button("Health Check"): self._perform_health_check() # PATTERN: Disconnect button if st.button("Disconnect"): self._disconnect() else: st.error("❌ Not Connected") # PATTERN: Connection form server_path = st.text_input( "Server Path", value=st.session_state.gui_session.server_path, help="Path to MCP server script" ) if st.button("Connect"): self._connect(server_path) def _connect(self, server_path: str): """Connect to MCP server""" try: with st.spinner("Connecting to server..."): # CRITICAL: Handle async in Streamlit client = MCPClient(server_path) asyncio.run(client.connect()) st.session_state.mcp_client = client st.session_state.gui_session.connected = True st.session_state.gui_session.server_path = server_path st.session_state.gui_session.available_tools = client.available_tools st.success("Connected successfully!") st.rerun() except Exception as e: st.error(f"Connection failed: {str(e)}") st.exception(e) def _disconnect(self): """Disconnect from MCP server""" if st.session_state.mcp_client: asyncio.run(st.session_state.mcp_client.disconnect()) st.session_state.mcp_client = None st.session_state.gui_session.connected = False st.session_state.gui_session.available_tools = [] st.success("Disconnected successfully!") st.rerun() def _perform_health_check(self): """Perform health check on connection""" if st.session_state.mcp_client: try: health = asyncio.run(st.session_state.mcp_client.health_check()) if health: st.success("Health check passed!") else: st.warning("Health check failed - connection may be unhealthy") except Exception as e: st.error(f"Health check error: {str(e)}") # Task 5: Tool Form Components # src/gui/components/tool_forms.py class ToolForms: """Manages tool-specific form interfaces""" def render(self): """Render tool selection and forms""" available_tools = st.session_state.gui_session.available_tools if not available_tools: st.warning("No tools available") return # PATTERN: Tool selection selected_tool = st.selectbox( "Select Tool", options=available_tools, help="Choose a tool to invoke" ) # PATTERN: Tool-specific forms if selected_tool == "roll_dice": self._render_dice_form() elif selected_tool == "get_weather": self._render_weather_form() elif selected_tool == "get_date": self._render_date_form() def _render_dice_form(self): """Render dice rolling form""" with st.form("dice_form"): st.subheader("🎲 Roll Dice") notation = st.text_input( "Dice Notation", value="2d6", help="Enter dice notation (e.g., 2d6, 1d20, 3d10)" ) # PATTERN: Help text with examples st.caption("Examples: 1d20 (single 20-sided die), 3d6 (three 6-sided dice)") submitted = st.form_submit_button("Roll Dice") if submitted: if self._validate_dice_notation(notation): self._execute_tool("roll_dice", {"notation": notation}) else: st.error("Invalid dice notation. Use format like '2d6' or '1d20'") def _render_weather_form(self): """Render weather lookup form""" with st.form("weather_form"): st.subheader("🌤️ Get Weather") location = st.text_input( "Location", value="San Francisco", help="Enter city name or coordinates (lat,lon)" ) # PATTERN: Common location examples st.caption("Examples: London, New York, 37.7749,-122.4194") submitted = st.form_submit_button("Get Weather") if submitted: if location.strip(): self._execute_tool("get_weather", {"location": location}) else: st.error("Please enter a location") def _render_date_form(self): """Render date/time lookup form""" with st.form("date_form"): st.subheader("🕐 Get Date & Time") timezone = st.selectbox( "Timezone", options=["UTC", "America/New_York", "America/Los_Angeles", "Europe/London", "Asia/Tokyo", "Australia/Sydney"], help="Select timezone or enter custom IANA timezone" ) custom_timezone = st.text_input( "Custom Timezone (optional)", placeholder="e.g., America/Chicago", help="Enter custom IANA timezone identifier" ) submitted = st.form_submit_button("Get Date & Time") if submitted: tz = custom_timezone.strip() if custom_timezone.strip() else timezone self._execute_tool("get_date", {"timezone": tz}) def _execute_tool(self, tool_name: str, arguments: dict): """Execute tool and update GUI state""" if not st.session_state.mcp_client: st.error("Not connected to server") return try: with st.spinner(f"Executing {tool_name}..."): # CRITICAL: Time execution for performance metrics start_time = time.time() # PATTERN: Use existing MCP client result = asyncio.run( st.session_state.mcp_client.invoke_tool(tool_name, arguments) ) execution_time = time.time() - start_time # PATTERN: Create interaction record interaction = GUIInteraction( tool_name=tool_name, arguments=arguments, request_payload={ "tool": tool_name, "arguments": arguments }, response_payload=result.model_dump(), success=result.success, error_message=result.error if not result.success else None, execution_time=execution_time ) # PATTERN: Update session state st.session_state.gui_session.interaction_history.append(interaction) # PATTERN: Display result if result.success: st.success(f"✅ {tool_name} executed successfully!") st.json(result.model_dump()) else: st.error(f"❌ {tool_name} failed: {result.error}") st.rerun() except Exception as e: st.error(f"Execution error: {str(e)}") st.exception(e) def _validate_dice_notation(self, notation: str) -> bool: """Validate dice notation format""" import re pattern = r"^(\d+)d(\d+)$" return bool(re.match(pattern, notation.strip().lower())) ``` ### Integration Points ```yaml DEPENDENCIES: - add to: pyproject.toml - packages: ["streamlit>=1.28.0"] - preserve: existing mcp, httpx, pydantic versions MAIN_ENTRY: - modify: src/main.py - pattern: "async def main() -> None:" - add gui command alongside existing server/client commands MAKEFILE: - add to: Makefile - command: "run_gui: uv run streamlit run src/gui/app.py" - integrate: GUI testing into existing test suite TESTING: - add to: tests/test_gui.py - pattern: "pytest fixtures for GUI component testing" - mock: Streamlit interactions for unit tests - integration: GUI with MCP client functionality ``` ## Validation Loop ### Level 1: Syntax & Style ```bash # Run these FIRST - fix any errors before proceeding uv run ruff check src/gui/ --fix uv run mypy src/gui/ uv run ruff format src/gui/ # Expected: No errors. If errors exist, read and fix them. ``` ### Level 2: Unit Tests ```python # CREATE tests/test_gui.py import pytest import asyncio from unittest.mock import Mock, patch from src.gui.models.gui_models import GUISession, GUIInteraction from src.gui.components.connection import ConnectionManager from src.gui.utils.validation import validate_dice_notation def test_gui_session_model(): """Test GUI session model validation""" session = GUISession() assert not session.connected assert session.server_path == "src/mcp_server/server.py" assert len(session.interaction_history) == 0 def test_gui_interaction_model(): """Test GUI interaction model""" interaction = GUIInteraction( tool_name="roll_dice", arguments={"notation": "2d6"}, request_payload={"tool": "roll_dice", "arguments": {"notation": "2d6"}}, response_payload={"success": True}, success=True ) assert interaction.tool_name == "roll_dice" assert interaction.success is True assert interaction.timestamp is not None def test_dice_notation_validation(): """Test dice notation validation""" assert validate_dice_notation("2d6") is True assert validate_dice_notation("1d20") is True assert validate_dice_notation("invalid") is False assert validate_dice_notation("2d") is False @patch('src.gui.components.connection.MCPClient') def test_connection_manager_connect(mock_client): """Test connection manager connection process""" mock_client_instance = Mock() mock_client_instance.available_tools = ["roll_dice", "get_weather"] mock_client.return_value = mock_client_instance # Test connection logic manager = ConnectionManager() # Mock Streamlit session state would go here # This would need to be adapted for actual Streamlit testing def test_json_formatting(): """Test JSON formatting utilities""" from src.gui.utils.formatting import format_json_for_display data = {"tool": "roll_dice", "result": {"values": [3, 5], "total": 8}} formatted = format_json_for_display(data) assert isinstance(formatted, str) assert "roll_dice" in formatted ``` ```bash # Run and iterate until passing: uv run pytest tests/test_gui.py -v # If failing: Read error, fix code, re-run ``` ### Level 3: Integration Test ```bash # Test Streamlit GUI manually # 1. Start MCP server in background uv run python -m src.mcp_server.server & # 2. Launch Streamlit GUI uv run streamlit run src/gui/app.py # 3. Test in browser (http://localhost:8501) # - Test connection to server # - Test each tool form (roll_dice, get_weather, get_date) # - Test error handling with invalid inputs # - Test session history functionality # - Test disconnection and reconnection # 4. Test GUI via make command make run_gui # Expected: GUI loads successfully, all tools work, history is maintained ``` ## Final Validation Checklist - [ ] All tests pass: `uv run pytest tests/ -v` - [ ] No linting errors: `uv run ruff check src/` - [ ] No type errors: `uv run mypy src/` - [ ] Streamlit GUI launches successfully via `make run_gui` - [ ] Connection to MCP server works through GUI - [ ] All three tools can be invoked through forms - [ ] Request/response payloads are displayed properly - [ ] Error handling shows user-friendly messages - [ ] Session history tracks all interactions - [ ] Connection status updates in real-time - [ ] Input validation prevents invalid submissions - [ ] JSON formatting is readable and highlighted - [ ] GUI is responsive and user-friendly - [ ] Example interaction files are created and valid ## Anti-Patterns to Avoid - ❌ Don't use synchronous functions in Streamlit async context - ❌ Don't forget to use st.session_state for persistent data - ❌ Don't ignore Streamlit's rerun behavior - handle state carefully - ❌ Don't hardcode server paths or tool configurations - ❌ Don't skip input validation in forms - ❌ Don't expose raw error messages to users - provide friendly alternatives - ❌ Don't forget to handle connection lifecycle properly - ❌ Don't use blocking operations without spinner feedback - ❌ Don't ignore JSON formatting for request/response display - ❌ Don't skip session history management --- ## Quality Score: 9/10 This PRP provides comprehensive context for one-pass implementation: - ✅ Complete Streamlit documentation and GUI patterns - ✅ Detailed existing MCP client/server implementation context - ✅ Specific GUI interface requirements and user experience design - ✅ Executable validation commands and testing approach - ✅ Clear task breakdown with pseudocode and implementation details - ✅ Anti-patterns and common pitfalls for Streamlit development - ✅ Integration points with existing codebase clearly defined - ✅ Session state management and async operation handling - ✅ Comprehensive error handling and user feedback patterns **Areas for improvement:** - Could include more specific Streamlit testing patterns - Could provide more detailed responsive design considerations **Confidence Level:** Very High - This PRP contains comprehensive technical detail about Streamlit development, complete integration context with existing MCP implementation, and clear validation steps for successful GUI implementation by an AI agent with access to the codebase and web search capabilities. ================================================ FILE: examples/mcp-server-client/context/templates/feature_base.md ================================================ # Feature description for: [ Initial template for new features ] ## FEATURE [Insert your feature here] ## EXAMPLES [Provide and explain examples that you have in the `/context/examples` folder] ## DOCUMENTATION [List out any documentation (web pages, sources for an MCP server like Crawl4AI RAG, etc.) that will need to be referenced during development] ## OTHER CONSIDERATIONS [Any other considerations or specific requirements - great place to include gotchas that you see AI coding assistants miss with your projects a lot] ================================================ FILE: examples/mcp-server-client/context/templates/prp_base.md ================================================ # "Base PRP Template v2 - Context-Rich with Validation Loops" ## Purpose Product Requirements Prompt (PRP) Template optimized for AI agents to implement features with sufficient context and self-validation capabilities to achieve working code through iterative refinement. ## Core Principles 1. **Context is King**: Include ALL necessary documentation, examples, and caveats 2. **Validation Loops**: Provide executable tests/lints the AI can run and fix 3. **Information Dense**: Use keywords and patterns from the codebase 4. **Progressive Success**: Start simple, validate, then enhance 5. **Global rules**: Be sure to follow all rules in CLAUDE.md --- ## Goal [What needs to be built - be specific about the end state and desires] ## Why - [Business value and user impact] - [Integration with existing features] - [Problems this solves and for whom] ## What [User-visible behavior and technical requirements] ### Success Criteria - [ ] [Specific measurable outcomes] ## All Needed Context ### Documentation & References (list all context needed to implement the feature) ```yaml # MUST READ - Include these in your context window - url: [Official API docs URL] why: [Specific sections/methods you'll need] - file: [path/to/example.py] why: [Pattern to follow, gotchas to avoid] - doc: [Library documentation URL] section: [Specific section about common pitfalls] critical: [Key insight that prevents common errors] - docfile: [PRPs/ai_docs/file.md] why: [docs that the user has pasted in to the project] ``` ### Current Codebase tree (run `tree` in the root of the project) to get an overview of the codebase ```bash ``` ### Desired Codebase tree with files to be added and responsibility of file ```bash ``` ### Known Gotchas of our codebase & Library Quirks ```python # CRITICAL: [Library name] requires [specific setup] # Example: FastAPI requires async functions for endpoints # Example: This ORM doesn't support batch inserts over 1000 records # Example: We use pydantic v2 and ``` ## Implementation Blueprint ### Data models and structure Create the core data models, we ensure type safety and consistency. ```python Examples: - orm models - pydantic models - pydantic schemas - pydantic validators ``` ### list of tasks to be completed to fullfill the PRP in the order they should be completed ```yaml Task 1: MODIFY src/existing_module.py: - FIND pattern: "class OldImplementation" - INJECT after line containing "def __init__" - PRESERVE existing method signatures CREATE src/new_feature.py: - MIRROR pattern from: src/similar_feature.py - MODIFY class name and core logic - KEEP error handling pattern identical ...(...) Task N: ... ``` ### Per task pseudocode as needed added to each task ```python # Task 1 # Pseudocode with CRITICAL details dont write entire code async def new_feature(param: str) -> Result: # PATTERN: Always validate input first (see src/validators.py) validated = validate_input(param) # raises ValidationError # GOTCHA: This library requires connection pooling async with get_connection() as conn: # see src/db/pool.py # PATTERN: Use existing retry decorator @retry(attempts=3, backoff=exponential) async def _inner(): # CRITICAL: API returns 429 if >10 req/sec await rate_limiter.acquire() return await external_api.call(validated) result = await _inner() # PATTERN: Standardized response format return format_response(result) # see src/utils/responses.py ``` ### Integration Points ```yaml DATABASE: - migration: "Add column 'feature_enabled' to users table" - index: "CREATE INDEX idx_feature_lookup ON users(feature_id)" CONFIG: - add to: config/settings.py - pattern: "FEATURE_TIMEOUT = int(os.getenv('FEATURE_TIMEOUT', '30'))" ROUTES: - add to: src/api/routes.py - pattern: "router.include_router(feature_router, prefix='/feature')" ``` ## Validation Loop ### Level 1: Syntax & Style ```bash # Run these FIRST - fix any errors before proceeding ruff check src/new_feature.py --fix # Auto-fix what's possible mypy src/new_feature.py # Type checking # Expected: No errors. If errors, READ the error and fix. ``` ### Level 2: Unit Tests each new feature/file/function use existing test patterns ```python # CREATE test_new_feature.py with these test cases: def test_happy_path(): """Basic functionality works""" result = new_feature("valid_input") assert result.status == "success" def test_validation_error(): """Invalid input raises ValidationError""" with pytest.raises(ValidationError): new_feature("") def test_external_api_timeout(): """Handles timeouts gracefully""" with mock.patch('external_api.call', side_effect=TimeoutError): result = new_feature("valid") assert result.status == "error" assert "timeout" in result.message ``` ```bash # Run and iterate until passing: uv run pytest test_new_feature.py -v # If failing: Read error, understand root cause, fix code, re-run (never mock to pass) ``` ### Level 3: Integration Test ```bash # Start the service uv run python -m src.main --dev # Test the endpoint curl -X POST http://localhost:8000/feature \ -H "Content-Type: application/json" \ -d '{"param": "test_value"}' # Expected: {"status": "success", "data": {...}} # If error: Check logs at logs/app.log for stack trace ``` ## Final validation Checklist - [ ] All tests pass: `uv run pytest tests/ -v` - [ ] No linting errors: `uv run ruff check src/` - [ ] No type errors: `uv run mypy src/` - [ ] Manual test successful: [specific curl/command] - [ ] Error cases handled gracefully - [ ] Logs are informative but not verbose - [ ] Documentation updated if needed --- ## Anti-Patterns to Avoid - ❌ Don't create new patterns when existing ones work - ❌ Don't skip validation because "it should work" - ❌ Don't ignore failing tests - fix them - ❌ Don't use sync functions in async context - ❌ Don't hardcode values that should be config - ❌ Don't catch all exceptions - be specific ================================================ FILE: examples/mcp-server-client/src/__init__.py ================================================ """Defines the application version.""" __version__ = "0.0.1" ================================================ FILE: examples/mcp-server-client/src/main.py ================================================ """Main entry point for the MCP server and client applications.""" import argparse import logging import sys from asyncio import run from src.mcp_client.cli import MCPClientCLI from src.mcp_server import run_server def setup_logging(level: str = "INFO") -> None: """Setup logging configuration.""" logging.basicConfig( level=getattr(logging, level.upper()), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) async def run_client_async(args) -> int: """Run the MCP client in async mode.""" # Create client CLI and run it cli = MCPClientCLI() # Build client arguments from parsed args client_args = [ "--server", args.server, "--log-level", args.log_level, "--timeout", str(args.timeout), ] if args.tool: client_args.append(args.tool) # Add tool-specific arguments if args.tool == "roll_dice": client_args.extend(["--notation", args.notation]) elif args.tool == "get_weather": client_args.extend(["--location", args.location]) elif args.tool == "get_date": client_args.extend(["--timezone", args.timezone]) # Run the client return await cli.run(client_args) def main() -> None: """Main entry point for MCP applications.""" parser = argparse.ArgumentParser( description="MCP Server and Client with dice, weather, and date/time tools", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Run MCP server %(prog)s server # Run MCP client %(prog)s client --server ./server.py roll_dice --notation 2d6 %(prog)s client --server ./server.py get_weather --location "San Francisco" %(prog)s client --server ./server.py get_date --timezone UTC # Launch Streamlit GUI %(prog)s gui %(prog)s gui --port 8502 """, ) # Add subcommands subparsers = parser.add_subparsers( dest="mode", help="Choose server or client mode", metavar="MODE" ) # Server subcommand server_parser = subparsers.add_parser("server", help="Run MCP server") server_parser.add_argument( "--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"], default="INFO", help="Set the logging level", ) server_parser.add_argument( "--version", action="version", version="MCP Server v1.0.0", ) # Client subcommand client_parser = subparsers.add_parser("client", help="Run MCP client") # GUI subcommand gui_parser = subparsers.add_parser("gui", help="Launch Streamlit GUI") gui_parser.add_argument( "--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"], default="INFO", help="Set the logging level", ) gui_parser.add_argument( "--port", type=int, default=8501, help="Port for Streamlit server (default: 8501)", ) client_parser.add_argument( "--server", required=True, help="Path to MCP server script" ) client_parser.add_argument( "--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"], default="INFO", help="Set logging level (default: INFO)", ) client_parser.add_argument( "--timeout", type=int, default=30, help="Connection timeout in seconds (default: 30)", ) # Tool subcommands for client tool_subparsers = client_parser.add_subparsers( dest="tool", help="Available tools", metavar="TOOL" ) # Roll dice tool dice_parser = tool_subparsers.add_parser( "roll_dice", help="Roll dice using standard notation" ) dice_parser.add_argument( "--notation", required=True, help="Dice notation (e.g., 2d6, 1d20, 3d10)" ) # Weather tool weather_parser = tool_subparsers.add_parser( "get_weather", help="Get current weather conditions" ) weather_parser.add_argument( "--location", required=True, help="Location name or coordinates (lat,lon)" ) # Date/time tool date_parser = tool_subparsers.add_parser( "get_date", help="Get current date and time" ) date_parser.add_argument( "--timezone", default="UTC", help="Timezone identifier (default: UTC)" ) args = parser.parse_args() # Check if mode is specified if not args.mode: parser.print_help() sys.exit(1) # Setup logging log_level = getattr(args, "log_level", "INFO") setup_logging(log_level) logger = logging.getLogger(__name__) try: if args.mode == "server": logger.info("Starting MCP Server application") # Run the MCP server (this will block until server shuts down) run_server() elif args.mode == "client": logger.info("Starting MCP Client application") # Run client in async mode exit_code = run(run_client_async(args)) sys.exit(exit_code) elif args.mode == "gui": logger.info("Starting Streamlit GUI application") # Launch Streamlit GUI import os import subprocess # Set environment variables for Streamlit env = os.environ.copy() env["STREAMLIT_SERVER_PORT"] = str(args.port) env["STREAMLIT_LOGGER_LEVEL"] = args.log_level # Run Streamlit cmd = [ "streamlit", "run", "src/gui/app.py", "--server.port", str(args.port), "--logger.level", args.log_level.lower(), ] subprocess.run(cmd, env=env) else: parser.print_help() sys.exit(1) except KeyboardInterrupt: logger.info("Application interrupted by user") sys.exit(0) except Exception as e: logger.error(f"Application error: {e}") sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: examples/mcp-server-client/src/py.typed ================================================ # PEP 561 – Distributing and Packaging Type Information # https://peps.python.org/pep-0561/ ================================================ FILE: examples/mcp-server-client/src/gui/__init__.py ================================================ """GUI module for MCP client interface.""" ================================================ FILE: examples/mcp-server-client/src/gui/app.py ================================================ """Main Streamlit application for MCP Tool Showcase.""" import sys from pathlib import Path # Add project root to Python path - must be done before importing local modules project_root = Path(__file__).parent.parent.parent sys.path.insert(0, str(project_root)) # ruff: noqa: E402 import streamlit as st from src.gui.components.connection import ConnectionManager from src.gui.components.history import HistoryManager from src.gui.components.tool_forms import ToolForms from src.gui.models.gui_models import GUISession def main() -> None: """Main Streamlit application entry point.""" st.set_page_config( page_title="MCP Tool Showcase", page_icon="🛠️", layout="wide", initial_sidebar_state="expanded", ) # Initialize session state if "gui_session" not in st.session_state: st.session_state.gui_session = GUISession() if "mcp_client" not in st.session_state: st.session_state.mcp_client = None # Main layout with sidebar with st.sidebar: st.title("🛠️ MCP Tool Showcase") st.markdown("---") # Connection management connection_manager = ConnectionManager() connection_manager.render() # Main content area col1, col2 = st.columns([1, 1]) with col1: st.header("Tool Invocation") if st.session_state.gui_session.connected: tool_forms = ToolForms() tool_forms.render() else: st.info("Please connect to the MCP server first") with col2: st.header("Request/Response") if st.session_state.gui_session.interaction_history: latest_interaction = st.session_state.gui_session.interaction_history[-1] st.subheader("Latest Request") st.json(latest_interaction.request_payload) st.subheader("Latest Response") st.json(latest_interaction.response_payload) else: st.info("No interactions yet") # History section st.header("Interaction History") history_manager = HistoryManager() history_manager.render() if __name__ == "__main__": main() ================================================ FILE: examples/mcp-server-client/src/gui/components/__init__.py ================================================ """GUI components for reusable interface elements.""" ================================================ FILE: examples/mcp-server-client/src/gui/components/connection.py ================================================ """Connection management component for MCP server.""" import logging import streamlit as st from src.gui.utils.mcp_wrapper import MCPConnectionManager logger = logging.getLogger(__name__) class ConnectionManager: """Manages MCP server connection in GUI.""" def render(self) -> None: """Render connection management interface.""" st.subheader("Connection Status") # Display current status if st.session_state.gui_session.connected: st.success("✅ Connected to MCP Server") st.write(f"Server: {st.session_state.gui_session.server_path}") tools = ", ".join(st.session_state.gui_session.available_tools) st.write(f"Available Tools: {tools}") # Health check button if st.button("Health Check"): self._perform_health_check() # Disconnect button if st.button("Disconnect"): self._disconnect() else: st.error("❌ Not Connected") # Connection form server_path = st.text_input( "Server Path", value=st.session_state.gui_session.server_path, help="Path to MCP server script", ) if st.button("Connect"): self._connect(server_path) def _connect(self, server_path: str) -> None: """Connect to MCP server.""" try: with st.spinner("Connecting to server..."): # Create connection manager if it doesn't exist if "mcp_connection_manager" not in st.session_state: st.session_state.mcp_connection_manager = MCPConnectionManager() manager = st.session_state.mcp_connection_manager # Connect to server success = manager.connect(server_path) if success: st.session_state.gui_session.connected = True st.session_state.gui_session.server_path = server_path st.session_state.gui_session.available_tools = ( manager.available_tools ) st.success("Connected successfully!") st.rerun() else: st.error("Failed to connect to server") except Exception as e: logger.error(f"Connection failed: {e}") st.error(f"Connection failed: {str(e)}") def _disconnect(self) -> None: """Disconnect from MCP server.""" if "mcp_connection_manager" in st.session_state: try: manager = st.session_state.mcp_connection_manager manager.disconnect() st.session_state.gui_session.connected = False st.session_state.gui_session.available_tools = [] st.success("Disconnected successfully!") st.rerun() except Exception as e: logger.error(f"Disconnect failed: {e}") st.error(f"Disconnect failed: {str(e)}") def _perform_health_check(self) -> None: """Perform health check on connection.""" if "mcp_connection_manager" in st.session_state: try: manager = st.session_state.mcp_connection_manager health = manager.health_check() if health: st.success("Health check passed!") else: st.warning("Health check failed - connection may be unhealthy") except Exception as e: logger.error(f"Health check error: {e}") st.error(f"Health check error: {str(e)}") ================================================ FILE: examples/mcp-server-client/src/gui/components/history.py ================================================ """History management component for interaction tracking.""" import streamlit as st from src.gui.models.gui_models import GUIInteraction class HistoryManager: """Manages session history display and interaction.""" def render(self) -> None: """Render interaction history interface.""" history = st.session_state.gui_session.interaction_history if not history: st.info( "No interactions yet. Connect to a server and use tools to see history." ) return # History controls col1, col2 = st.columns([1, 1]) with col1: st.write(f"**Total Interactions:** {len(history)}") with col2: if st.button("Clear History"): st.session_state.gui_session.interaction_history = [] st.rerun() # Display history for i, interaction in enumerate(reversed(history)): self._render_interaction(i, interaction) def _render_interaction(self, index: int, interaction: GUIInteraction) -> None: """Render a single interaction.""" # Create expander with status indicator status_icon = "✅" if interaction.success else "❌" timestamp = interaction.timestamp.strftime("%H:%M:%S") with st.expander( f"{status_icon} {interaction.tool_name} - {timestamp}", expanded=index == 0, # Expand latest interaction ): # Basic info col1, col2 = st.columns([1, 1]) with col1: st.write(f"**Tool:** {interaction.tool_name}") st.write( f"**Status:** {'Success' if interaction.success else 'Failed'}" ) if interaction.execution_time: st.write(f"**Execution Time:** {interaction.execution_time:.2f}s") with col2: timestamp_str = interaction.timestamp.strftime("%Y-%m-%d %H:%M:%S") st.write(f"**Timestamp:** {timestamp_str}") if interaction.error_message: st.write(f"**Error:** {interaction.error_message}") # Request/Response payloads req_col, resp_col = st.columns([1, 1]) with req_col: st.subheader("Request") st.json(interaction.request_payload) with resp_col: st.subheader("Response") st.json(interaction.response_payload) ================================================ FILE: examples/mcp-server-client/src/gui/components/tool_forms.py ================================================ """Tool-specific form components for MCP tools.""" import logging import re import time import streamlit as st from src.gui.models.gui_models import GUIInteraction logger = logging.getLogger(__name__) class ToolForms: """Manages tool-specific form interfaces.""" def render(self) -> None: """Render tool selection and forms.""" available_tools = st.session_state.gui_session.available_tools if not available_tools: st.warning("No tools available") return # Tool selection selected_tool = st.selectbox( "Select Tool", options=available_tools, help="Choose a tool to invoke" ) # Tool-specific forms if selected_tool == "roll_dice": self._render_dice_form() elif selected_tool == "get_weather": self._render_weather_form() elif selected_tool == "get_date": self._render_date_form() def _render_dice_form(self) -> None: """Render dice rolling form.""" with st.form("dice_form"): st.subheader("🎲 Roll Dice") notation = st.text_input( "Dice Notation", value="2d6", help="Enter dice notation (e.g., 2d6, 1d20, 3d10)", ) # Help text with examples st.caption("Examples: 1d20 (single 20-sided die), 3d6 (three 6-sided dice)") submitted = st.form_submit_button("Roll Dice") if submitted: if self._validate_dice_notation(notation): self._execute_tool("roll_dice", {"notation": notation}) else: st.error("Invalid dice notation. Use format like '2d6' or '1d20'") def _render_weather_form(self) -> None: """Render weather lookup form.""" with st.form("weather_form"): st.subheader("🌤️ Get Weather") location = st.text_input( "Location", value="San Francisco", help="Enter city name or coordinates (lat,lon)", ) # Common location examples st.caption("Examples: London, New York, 37.7749,-122.4194") submitted = st.form_submit_button("Get Weather") if submitted: if location.strip(): self._execute_tool("get_weather", {"location": location}) else: st.error("Please enter a location") def _render_date_form(self) -> None: """Render date/time lookup form.""" with st.form("date_form"): st.subheader("🕐 Get Date & Time") timezone = st.selectbox( "Timezone", options=[ "UTC", "America/New_York", "America/Los_Angeles", "Europe/London", "Asia/Tokyo", "Australia/Sydney", ], help="Select timezone or enter custom IANA timezone", ) custom_timezone = st.text_input( "Custom Timezone (optional)", placeholder="e.g., America/Chicago", help="Enter custom IANA timezone identifier", ) submitted = st.form_submit_button("Get Date & Time") if submitted: tz = custom_timezone.strip() if custom_timezone.strip() else timezone self._execute_tool("get_date", {"timezone": tz}) def _execute_tool(self, tool_name: str, arguments: dict[str, str]) -> None: """Execute tool and update GUI state.""" if "mcp_connection_manager" not in st.session_state: st.error("Not connected to server") return try: with st.spinner(f"Executing {tool_name}..."): # Time execution for performance metrics start_time = time.time() # Use connection manager manager = st.session_state.mcp_connection_manager result = manager.invoke_tool(tool_name, arguments) execution_time = time.time() - start_time # Create interaction record interaction = GUIInteraction( tool_name=tool_name, arguments=arguments, request_payload={"tool": tool_name, "arguments": arguments}, response_payload=result, success=result.get("success", False), error_message=result.get("error") if not result.get("success", False) else None, execution_time=execution_time, ) # Update session state st.session_state.gui_session.interaction_history.append(interaction) # Display result if result.get("success", False): st.success(f"✅ {tool_name} executed successfully!") st.json(result) else: st.error( f"❌ {tool_name} failed: {result.get('error', 'Unknown error')}" ) st.rerun() except Exception as e: logger.error(f"Tool execution error: {e}") st.error(f"Execution error: {str(e)}") def _validate_dice_notation(self, notation: str) -> bool: """Validate dice notation format.""" pattern = r"^(\d+)d(\d+)$" return bool(re.match(pattern, notation.strip().lower())) ================================================ FILE: examples/mcp-server-client/src/gui/models/__init__.py ================================================ """GUI models for state management and data validation.""" ================================================ FILE: examples/mcp-server-client/src/gui/models/gui_models.py ================================================ """GUI-specific models for state management and type safety.""" from datetime import datetime from typing import Any from pydantic import BaseModel, Field class GUIInteraction(BaseModel): """Single tool interaction record for GUI history.""" timestamp: datetime = Field(default_factory=datetime.now) tool_name: str arguments: dict[str, Any] request_payload: dict[str, Any] response_payload: dict[str, Any] success: bool error_message: str | None = None execution_time: float | None = None class GUISession(BaseModel): """GUI session state management.""" connected: bool = False server_path: str = "src/mcp_server/server.py" available_tools: list[str] = Field(default_factory=list) interaction_history: list[GUIInteraction] = Field(default_factory=list) current_tool: str | None = None class ConnectionStatus(BaseModel): """Connection status information.""" connected: bool = False server_path: str last_health_check: datetime | None = None available_tools: list[str] = Field(default_factory=list) error_message: str | None = None ================================================ FILE: examples/mcp-server-client/src/gui/utils/__init__.py ================================================ """GUI utilities for formatting and validation.""" ================================================ FILE: examples/mcp-server-client/src/gui/utils/async_helper.py ================================================ """Async helper utilities for Streamlit GUI.""" import asyncio import threading from collections.abc import Awaitable from typing import TypeVar T = TypeVar("T") def run_async(coro: Awaitable[T]) -> T: """Run an async function in Streamlit context. This handles the case where Streamlit might already have an event loop running. Args: coro: The coroutine to run Returns: The result of the coroutine """ try: # Try to get the current event loop # loop = asyncio.get_running_loop() # If we're already in an event loop, we need to run in a new thread # with its own event loop result = None exception = None def run_in_new_loop(): nonlocal result, exception try: new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) try: result = new_loop.run_until_complete(coro) finally: new_loop.close() except Exception as e: exception = e thread = threading.Thread(target=run_in_new_loop) thread.start() thread.join() if exception: raise exception return result except RuntimeError: # No event loop running, can use asyncio.run directly return asyncio.run(coro) class AsyncContextManager: """Helper to manage async context across multiple calls.""" def __init__(self): self._client = None self._loop = None self._thread = None def run_async(self, coro: Awaitable[T]) -> T: """Run async function maintaining context.""" return run_async(coro) ================================================ FILE: examples/mcp-server-client/src/gui/utils/formatting.py ================================================ """Formatting utilities for GUI display.""" import json from typing import Any def format_json_for_display(data: Any, indent: int = 2) -> str: """Format JSON data for display with proper indentation. Args: data: Data to format as JSON indent: Number of spaces for indentation Returns: Formatted JSON string """ try: return json.dumps(data, indent=indent, ensure_ascii=False) except (TypeError, ValueError) as e: return f"Error formatting JSON: {str(e)}" def format_error_message(error: str) -> str: """Format error messages for user-friendly display. Args: error: Raw error message Returns: Formatted error message """ # Remove common Python error prefixes error = error.replace("Exception: ", "") error = error.replace("Error: ", "") # Capitalize first letter if error: error = error[0].upper() + error[1:] return error def format_execution_time(seconds: float) -> str: """Format execution time for display. Args: seconds: Execution time in seconds Returns: Formatted time string """ if seconds < 1: return f"{seconds * 1000:.0f}ms" elif seconds < 60: return f"{seconds:.2f}s" else: minutes = int(seconds // 60) remaining_seconds = seconds % 60 return f"{minutes}m {remaining_seconds:.1f}s" def truncate_text(text: str, max_length: int = 100) -> str: """Truncate text with ellipsis if too long. Args: text: Text to truncate max_length: Maximum length before truncation Returns: Truncated text with ellipsis if needed """ if len(text) <= max_length: return text return text[: max_length - 3] + "..." ================================================ FILE: examples/mcp-server-client/src/gui/utils/mcp_wrapper.py ================================================ """MCP client wrapper for Streamlit GUI with persistent connection.""" import asyncio import logging import threading import time from queue import Empty, Queue from typing import Any from src.mcp_client.client import MCPClient logger = logging.getLogger(__name__) class MCPConnectionManager: """Manages a persistent MCP connection in a background thread.""" def __init__(self): self._client = None self._thread = None self._loop = None self._connected = False self._available_tools = [] self._request_queue = Queue() self._response_queue = Queue() self._shutdown_event = threading.Event() def connect(self, server_path: str) -> bool: """Connect to MCP server in background thread.""" if self._connected: self.disconnect() self._shutdown_event.clear() self._thread = threading.Thread( target=self._run_connection, args=(server_path,) ) self._thread.daemon = True self._thread.start() # Wait for connection to establish (with timeout) start_time = time.time() while not self._connected and time.time() - start_time < 10: time.sleep(0.1) return self._connected def disconnect(self) -> None: """Disconnect from MCP server.""" if self._thread and self._thread.is_alive(): self._shutdown_event.set() # Send disconnect command self._request_queue.put({"action": "disconnect"}) self._thread.join(timeout=5) self._connected = False self._available_tools = [] def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: """Invoke a tool and return the result.""" if not self._connected: return { "success": False, "error": "Not connected to server", "tool_name": tool_name, "arguments": arguments, } # Send request request_id = f"{tool_name}_{time.time()}" self._request_queue.put( { "action": "invoke_tool", "id": request_id, "tool_name": tool_name, "arguments": arguments, } ) # Wait for response (with timeout) start_time = time.time() while time.time() - start_time < 30: # 30 second timeout try: response = self._response_queue.get(timeout=1) if response.get("id") == request_id: return response.get( "result", { "success": False, "error": "No result in response", "tool_name": tool_name, "arguments": arguments, }, ) except Empty: continue return { "success": False, "error": "Request timeout", "tool_name": tool_name, "arguments": arguments, } def health_check(self) -> bool: """Check if connection is healthy.""" if not self._connected: return False # Send health check request request_id = f"health_{time.time()}" self._request_queue.put({"action": "health_check", "id": request_id}) # Wait for response start_time = time.time() while time.time() - start_time < 5: # 5 second timeout try: response = self._response_queue.get(timeout=1) if response.get("id") == request_id: return response.get("result", False) except Empty: continue return False @property def connected(self) -> bool: """Check if connected.""" return self._connected @property def available_tools(self) -> list[str]: """Get available tools.""" return self._available_tools.copy() def _run_connection(self, server_path: str) -> None: """Run the connection in background thread with its own event loop.""" try: # Create new event loop for this thread self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) # Run the connection handler self._loop.run_until_complete(self._connection_handler(server_path)) except Exception as e: logger.error(f"Connection thread error: {e}") finally: if self._loop: self._loop.close() self._connected = False async def _connection_handler(self, server_path: str) -> None: """Handle the MCP connection and requests.""" try: # Create and connect client self._client = MCPClient(server_path) await self._client.connect() self._connected = True self._available_tools = self._client.available_tools.copy() logger.info( f"Connected to MCP server. Available tools: {self._available_tools}" ) # Process requests until shutdown while not self._shutdown_event.is_set(): try: # Check for requests (non-blocking) request = self._request_queue.get(timeout=0.1) if request["action"] == "disconnect": break elif request["action"] == "invoke_tool": await self._handle_invoke_tool(request) elif request["action"] == "health_check": await self._handle_health_check(request) except Empty: # No request, continue continue except Exception as e: logger.error(f"Error processing request: {e}") except Exception as e: logger.error(f"Connection handler error: {e}") self._response_queue.put( { "id": "connection_error", "result": {"success": False, "error": str(e)}, } ) finally: # Clean up if self._client: try: await self._client.disconnect() except Exception as e: logger.error(f"Error disconnecting client: {e}") self._connected = False async def _handle_invoke_tool(self, request: dict[str, Any]) -> None: """Handle tool invocation request.""" try: result = await self._client.invoke_tool( request["tool_name"], request["arguments"] ) self._response_queue.put( { "id": request["id"], "result": result.model_dump() if hasattr(result, "model_dump") else { "success": True, "result": result, "tool_name": request["tool_name"], "arguments": request["arguments"], }, } ) except Exception as e: logger.error(f"Tool invocation error: {e}") self._response_queue.put( { "id": request["id"], "result": { "success": False, "error": str(e), "tool_name": request["tool_name"], "arguments": request["arguments"], }, } ) async def _handle_health_check(self, request: dict[str, Any]) -> None: """Handle health check request.""" try: health = await self._client.health_check() self._response_queue.put({"id": request["id"], "result": health}) except Exception as e: logger.error(f"Health check error: {e}") self._response_queue.put({"id": request["id"], "result": False}) ================================================ FILE: examples/mcp-server-client/src/gui/utils/validation.py ================================================ """Validation utilities for GUI input.""" import re def validate_dice_notation(notation: str) -> bool: """Validate dice notation format. Args: notation: Dice notation string (e.g., "2d6", "1d20") Returns: True if valid, False otherwise """ if not notation or not isinstance(notation, str): return False # Pattern: number + 'd' + number pattern = r"^(\d+)d(\d+)$" match = re.match(pattern, notation.strip().lower()) if not match: return False # Check reasonable limits num_dice = int(match.group(1)) num_sides = int(match.group(2)) # Validate ranges if num_dice < 1 or num_dice > 100: return False if num_sides < 2 or num_sides > 1000: return False return True def validate_location(location: str) -> bool: """Validate location input for weather lookup. Args: location: Location string (city name or coordinates) Returns: True if valid, False otherwise """ if not location or not isinstance(location, str): return False location = location.strip() # Check for coordinates format (lat,lon) coord_pattern = r"^-?\d+\.?\d*,-?\d+\.?\d*$" if re.match(coord_pattern, location): return True # Check for city name (at least 2 characters, letters and spaces) city_pattern = r"^[a-zA-Z\s]{2,}$" if re.match(city_pattern, location): return True return False def validate_timezone(timezone: str) -> bool: """Validate timezone identifier. Args: timezone: Timezone string (IANA format) Returns: True if valid, False otherwise """ if not timezone or not isinstance(timezone, str): return False timezone = timezone.strip() # Common timezone patterns common_timezones = [ "UTC", "GMT", "EST", "PST", "CST", "MST", "America/New_York", "America/Los_Angeles", "America/Chicago", "Europe/London", "Europe/Paris", "Asia/Tokyo", "Australia/Sydney", ] if timezone in common_timezones: return True # IANA timezone format: Area/City or Area/Region/City iana_pattern = r"^[A-Za-z_]+/[A-Za-z_]+(?:/[A-Za-z_]+)?$" return bool(re.match(iana_pattern, timezone)) def validate_server_path(path: str) -> str | None: """Validate server path and return error message if invalid. Args: path: Server path string Returns: Error message if invalid, None if valid """ if not path or not isinstance(path, str): return "Server path is required" path = path.strip() if not path: return "Server path cannot be empty" if not path.endswith(".py"): return "Server path must be a Python file (.py)" # Basic path validation if ".." in path or path.startswith("/"): return "Server path should be relative to project root" return None ================================================ FILE: examples/mcp-server-client/src/mcp_client/__init__.py ================================================ """MCP Client implementation for tool invocation.""" from .cli import MCPClientCLI from .client import MCPClient __all__ = ["MCPClient", "MCPClientCLI"] ================================================ FILE: examples/mcp-server-client/src/mcp_client/cli.py ================================================ """CLI interface for MCP client tool invocation.""" import argparse import asyncio import json import logging import sys from typing import Any from .client import MCPClient from .models.responses import ClientToolResult # Configure logging logger = logging.getLogger(__name__) class MCPClientCLI: """CLI interface for MCP client tool invocation.""" def __init__(self) -> None: """Initialize CLI interface.""" self.parser = self._create_parser() self.client: MCPClient | None = None def _create_parser(self) -> argparse.ArgumentParser: """Create argument parser with subcommands. Returns: Configured argument parser """ parser = argparse.ArgumentParser( description="MCP Client for tool invocation", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""Examples: %(prog)s --server ./server.py roll_dice --notation 2d6 %(prog)s --server ./server.py get_weather --location "San Francisco" %(prog)s --server ./server.py get_date --timezone UTC """, ) # Global arguments parser.add_argument("--server", required=True, help="Path to MCP server script") parser.add_argument( "--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"], default="INFO", help="Set logging level (default: INFO)", ) parser.add_argument( "--timeout", type=int, default=30, help="Connection timeout in seconds (default: 30)", ) # Subcommands for tools subparsers = parser.add_subparsers( dest="tool", help="Available tools", metavar="TOOL" ) # Roll dice tool dice_parser = subparsers.add_parser( "roll_dice", help="Roll dice using standard notation" ) dice_parser.add_argument( "--notation", required=True, help="Dice notation (e.g., 2d6, 1d20, 3d10)" ) # Weather tool weather_parser = subparsers.add_parser( "get_weather", help="Get current weather conditions" ) weather_parser.add_argument( "--location", required=True, help="Location name or coordinates (lat,lon)" ) # Date/time tool date_parser = subparsers.add_parser( "get_date", help="Get current date and time" ) date_parser.add_argument( "--timezone", default="UTC", help="Timezone identifier (default: UTC)" ) return parser def _setup_logging(self, level: str) -> None: """Setup logging configuration. Args: level: Logging level string """ logging.basicConfig( level=getattr(logging, level.upper()), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def _build_tool_arguments(self, args: argparse.Namespace) -> dict[str, Any]: """Build tool arguments from parsed CLI arguments. Args: args: Parsed command line arguments Returns: Dictionary of tool arguments """ if args.tool == "roll_dice": return {"notation": args.notation} elif args.tool == "get_weather": return {"location": args.location} elif args.tool == "get_date": return {"timezone": args.timezone} else: return {} def _display_success(self, result: ClientToolResult) -> None: """Display successful tool result. Args: result: Successful tool result """ print(f"✅ {result.tool_name} executed successfully:") print() # Handle MCP response format if result.result and hasattr(result.result, "content"): content = result.result.content if isinstance(content, list): for item in content: if isinstance(item, dict) and "text" in item: print(item["text"]) else: print(json.dumps(item, indent=2)) else: print(json.dumps(content, indent=2)) else: print(json.dumps(result.result, indent=2)) def _display_error(self, result: ClientToolResult) -> None: """Display tool execution error. Args: result: Failed tool result """ print(f"❌ {result.tool_name} failed:") print(f"Error: {result.error}") print() print(f"Tool: {result.tool_name}") print(f"Arguments: {json.dumps(result.arguments, indent=2)}") def _display_connection_error(self, error: Exception) -> None: """Display connection error. Args: error: Connection error """ print("❌ Connection failed:") print(f"Error: {error}") print() print("Troubleshooting:") print("1. Check that the server script exists") print("2. Verify the server script is executable") print("3. Ensure required dependencies are installed") print("4. Check that the server starts correctly") async def run(self, args: list[str] | None = None) -> int: """Run CLI with provided arguments. Args: args: Command line arguments (uses sys.argv if None) Returns: Exit code (0 for success, 1 for error) """ try: # Parse arguments parsed_args = self.parser.parse_args(args) # Setup logging self._setup_logging(parsed_args.log_level) # Check if tool specified if not parsed_args.tool: self.parser.print_help() return 1 # Create client self.client = MCPClient(parsed_args.server) # Connect to server with timeout try: await asyncio.wait_for( self.client.connect(), timeout=parsed_args.timeout ) except TimeoutError: print(f"❌ Connection timeout after {parsed_args.timeout} seconds") return 1 except Exception as e: self._display_connection_error(e) return 1 # Build tool arguments tool_args = self._build_tool_arguments(parsed_args) # Invoke tool result = await self.client.invoke_tool(parsed_args.tool, tool_args) # Display result if result.success: self._display_success(result) return 0 else: self._display_error(result) return 1 except KeyboardInterrupt: print("\n❌ Interrupted by user") return 130 except Exception as e: logger.error(f"Unexpected error: {e}") print(f"❌ Unexpected error: {e}") return 1 finally: # Cleanup if self.client: await self.client.disconnect() async def main() -> int: """Main CLI entry point. Returns: Exit code """ cli = MCPClientCLI() return await cli.run() if __name__ == "__main__": sys.exit(asyncio.run(main())) ================================================ FILE: examples/mcp-server-client/src/mcp_client/client.py ================================================ """Main MCP client class for tool invocation.""" import logging from typing import Any from .models.responses import ClientToolResult from .transport import MCPTransport # Configure logging logger = logging.getLogger(__name__) class MCPClient: """MCP client for connecting to servers and invoking tools.""" def __init__(self, server_path: str): """Initialize MCP client. Args: server_path: Path to the MCP server script """ self.server_path = server_path self.transport = MCPTransport(server_path) self._connected = False @property def connected(self) -> bool: """Check if client is connected to server.""" return self._connected and self.transport.connected @property def available_tools(self) -> list[str]: """Get list of available tools.""" return self.transport.available_tools async def connect(self) -> None: """Connect to MCP server. Raises: FileNotFoundError: If server script doesn't exist ConnectionError: If connection fails ValueError: If server script type is not supported """ logger.info(f"Connecting to MCP server: {self.server_path}") try: await self.transport.connect() self._connected = True logger.info( f"Connected successfully. Available tools: {self.available_tools}" ) except Exception as e: logger.error(f"Failed to connect: {e}") raise async def disconnect(self) -> None: """Disconnect from MCP server.""" logger.info("Disconnecting from MCP server") await self.transport.disconnect() self._connected = False async def invoke_tool( self, tool_name: str, arguments: dict[str, Any] ) -> ClientToolResult: """Invoke a tool on the connected server. Args: tool_name: Name of the tool to invoke arguments: Arguments to pass to the tool Returns: ClientToolResult with success status and result or error """ logger.info(f"Invoking tool: {tool_name} with arguments: {arguments}") # Check if connected if not self.connected: error_msg = "Not connected to server" logger.error(error_msg) return ClientToolResult( success=False, result=None, error=error_msg, tool_name=tool_name, arguments=arguments, ) # Check if tool is available if tool_name not in self.available_tools: error_msg = ( f"Tool '{tool_name}' not available. " f"Available tools: {self.available_tools}" ) logger.error(error_msg) return ClientToolResult( success=False, result=None, error=error_msg, tool_name=tool_name, arguments=arguments, ) try: # Call the tool through transport result = await self.transport.call_tool(tool_name, arguments) # Process the result logger.info(f"Tool '{tool_name}' executed successfully") return ClientToolResult( success=True, result=result, error=None, tool_name=tool_name, arguments=arguments, ) except Exception as e: error_msg = f"Tool execution failed: {str(e)}" logger.error(error_msg) return ClientToolResult( success=False, result=None, error=error_msg, tool_name=tool_name, arguments=arguments, ) async def health_check(self) -> bool: """Check if connection is healthy. Returns: True if connection is healthy, False otherwise """ if not self.connected: return False return await self.transport.health_check() async def __aenter__(self) -> "MCPClient": """Async context manager entry.""" await self.connect() return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async context manager exit.""" await self.disconnect() class MCPClientError(Exception): """Base exception for MCP client errors.""" pass class MCPConnectionError(MCPClientError): """Raised when connection to MCP server fails.""" pass class MCPToolError(MCPClientError): """Raised when tool execution fails.""" pass ================================================ FILE: examples/mcp-server-client/src/mcp_client/transport.py ================================================ """Transport layer for MCP client connections.""" import os from contextlib import AsyncExitStack from typing import Any from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client class MCPTransport: """Handles MCP server connections via stdio transport.""" def __init__(self, server_path: str): """Initialize transport with server path. Args: server_path: Path to the MCP server script """ self.server_path = server_path self.session: ClientSession | None = None self.exit_stack: AsyncExitStack | None = None self.connected = False self.available_tools: list[str] = [] async def connect(self) -> None: """Connect to MCP server via stdio transport. Raises: FileNotFoundError: If server script doesn't exist ConnectionError: If connection fails ValueError: If server script type is not supported """ # Validate server script exists if not os.path.exists(self.server_path): raise FileNotFoundError(f"Server script not found: {self.server_path}") # Determine server type and command if self.server_path.endswith(".py"): # Use uv run python for proper environment command = "uv" args = ["run", "python", self.server_path] elif self.server_path.endswith(".js"): command = "node" args = [self.server_path] else: raise ValueError(f"Unsupported server script type: {self.server_path}") # Create server parameters server_params = StdioServerParameters(command=command, args=args, env=None) try: # Setup resource management self.exit_stack = AsyncExitStack() # Connect to server read, write = await self.exit_stack.enter_async_context( stdio_client(server_params) ) # Create session self.session = await self.exit_stack.enter_async_context( ClientSession(read, write) ) # Initialize connection await self.session.initialize() # Discover available tools tools_response = await self.session.list_tools() self.available_tools = [tool.name for tool in tools_response.tools] self.connected = True except Exception as e: # Clean up on connection failure if self.exit_stack: await self.exit_stack.aclose() self.exit_stack = None raise ConnectionError(f"Failed to connect to server: {e}") async def disconnect(self) -> None: """Disconnect from MCP server.""" if self.exit_stack: await self.exit_stack.aclose() self.exit_stack = None self.session = None self.connected = False self.available_tools = [] async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: """Call a tool on the connected server. Args: tool_name: Name of the tool to call arguments: Arguments to pass to the tool Returns: Tool response content Raises: RuntimeError: If not connected to server ValueError: If tool is not available """ if not self.connected or not self.session: raise RuntimeError("Not connected to server") if tool_name not in self.available_tools: raise ValueError( f"Tool '{tool_name}' not available. " f"Available tools: {self.available_tools}" ) # Call the tool result = await self.session.call_tool(tool_name, arguments) return result async def health_check(self) -> bool: """Check if connection is healthy. Returns: True if connection is healthy, False otherwise """ if not self.connected or not self.session: return False try: # Try to list tools as a health check await self.session.list_tools() return True except Exception: return False async def __aenter__(self) -> "MCPTransport": """Async context manager entry.""" await self.connect() return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async context manager exit.""" await self.disconnect() ================================================ FILE: examples/mcp-server-client/src/mcp_client/models/__init__.py ================================================ """MCP Client models for type safety and validation.""" from .responses import ClientToolResult, MCPToolResponse __all__ = ["ClientToolResult", "MCPToolResponse"] ================================================ FILE: examples/mcp-server-client/src/mcp_client/models/responses.py ================================================ """Client-specific response models for MCP tool invocation.""" from typing import Any from pydantic import BaseModel, Field class MCPToolResponse(BaseModel): """Response from MCP tool execution.""" content: list[dict[str, Any]] = Field(..., description="Response content") isError: bool = Field(False, description="Whether response indicates error") class ClientToolResult(BaseModel): """Processed tool result for client consumption.""" success: bool = Field(..., description="Whether tool execution was successful") result: Any | None = Field(None, description="Tool execution result") error: str | None = Field(None, description="Error message if execution failed") tool_name: str = Field(..., description="Name of the tool that was executed") arguments: dict[str, Any] = Field(..., description="Arguments passed to the tool") class ClientSession(BaseModel): """Client session information.""" server_path: str = Field(..., description="Path to the MCP server script") connected: bool = Field(False, description="Whether client is connected to server") available_tools: list[str] = Field( default_factory=list, description="List of available tools" ) ================================================ FILE: examples/mcp-server-client/src/mcp_server/__init__.py ================================================ """MCP server package.""" from .server import mcp, run_server __all__ = ["mcp", "run_server"] ================================================ FILE: examples/mcp-server-client/src/mcp_server/server.py ================================================ """MCP server implementation with dice, weather, and date/time tools.""" import logging import sys from pathlib import Path from typing import Any # Add project root to Python path project_root = Path(__file__).parent.parent.parent sys.path.insert(0, str(project_root)) from mcp.server.fastmcp import FastMCP from src.mcp_server.tools.date_time import DateTimeTool from src.mcp_server.tools.dice import DiceRollTool from src.mcp_server.tools.weather import WeatherTool # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Create MCP server instance mcp = FastMCP("dice-weather-datetime-server") # Initialize tool instances dice_tool = DiceRollTool() weather_tool = WeatherTool() datetime_tool = DateTimeTool() @mcp.tool() async def roll_dice(notation: str) -> dict[str, Any]: """Roll dice using standard notation like '2d6' or '1d20'. Args: notation: Dice notation (e.g., "2d6", "1d20", "3d10") Returns: Dict containing dice roll results, total, and formatted display """ logger.info(f"Tool call: roll_dice(notation='{notation}')") return await dice_tool.safe_execute(notation=notation) @mcp.tool() async def get_weather(location: str) -> dict[str, Any]: """Get current weather conditions for a location. Args: location: City name or coordinates (lat,lon) Returns: Dict containing weather data including temperature, condition, and wind speed """ logger.info(f"Tool call: get_weather(location='{location}')") return await weather_tool.safe_execute(location=location) @mcp.tool() async def get_date(timezone: str = "UTC") -> dict[str, Any]: """Get current date and time for a specific timezone. Args: timezone: Timezone identifier (e.g., "UTC", "America/New_York") or alias Returns: Dict containing current date/time in ISO 8601 format with timezone info """ logger.info(f"Tool call: get_date(timezone='{timezone}')") return await datetime_tool.safe_execute(timezone=timezone) @mcp.resource("mcp://tools/help") async def get_help() -> str: """Get help information about available tools.""" help_text = """ 🎲 **MCP Server - Available Tools** **roll_dice** - Roll dice using standard notation - Usage: roll_dice(notation="2d6") - Examples: "1d20", "3d6", "2d10" - Returns individual values and total **get_weather** - Get current weather conditions - Usage: get_weather(location="San Francisco") - Supports city names or coordinates (lat,lon) - Returns temperature, condition, wind speed **get_date** - Get current date and time - Usage: get_date(timezone="UTC") - Supports IANA timezones and common aliases - Returns ISO 8601 formatted datetime **Examples:** - roll_dice("2d6") → Roll two six-sided dice - get_weather("London") → Weather for London - get_date("America/New_York") → NYC current time """ return help_text async def cleanup_server(): """Cleanup server resources.""" logger.info("Cleaning up server resources...") try: await weather_tool.cleanup() logger.info("Server cleanup completed") except Exception as e: logger.error(f"Error during cleanup: {e}") # Server lifecycle management async def startup(): """Server startup handler.""" logger.info("MCP Server starting up...") logger.info("Tools available: roll_dice, get_weather, get_date") async def shutdown(): """Server shutdown handler.""" logger.info("MCP Server shutting down...") await cleanup_server() def run_server(): """Run the MCP server.""" try: logger.info("Starting MCP server...") mcp.run() except KeyboardInterrupt: logger.info("Server interrupted by user") except Exception as e: logger.error(f"Server error: {e}") raise if __name__ == "__main__": run_server() ================================================ FILE: examples/mcp-server-client/src/mcp_server/models/__init__.py ================================================ """MCP server data models.""" from .requests import ( DateTimeRequest, DateTimeResponse, DiceRollRequest, DiceRollResponse, MCPError, MCPRequest, MCPResponse, ToolCallRequest, ToolCallResponse, WeatherRequest, WeatherResponse, ) __all__ = [ "DateTimeRequest", "DateTimeResponse", "DiceRollRequest", "DiceRollResponse", "MCPError", "MCPRequest", "MCPResponse", "ToolCallRequest", "ToolCallResponse", "WeatherRequest", "WeatherResponse", ] ================================================ FILE: examples/mcp-server-client/src/mcp_server/models/requests.py ================================================ """Pydantic models for MCP server request/response validation.""" import re from typing import Any from pydantic import BaseModel, Field, field_validator class MCPRequest(BaseModel): """Base MCP request structure following JSON-RPC 2.0 format.""" jsonrpc: str = "2.0" method: str params: dict | None = None id: str | int | None = None class MCPResponse(BaseModel): """Base MCP response structure following JSON-RPC 2.0 format.""" jsonrpc: str = "2.0" id: str | int | None = None result: Any | None = None error: dict | None = None class MCPError(BaseModel): """MCP error structure.""" code: int message: str data: dict | None = None class DiceRollRequest(BaseModel): """Dice roll tool request with notation validation.""" notation: str = Field(..., description="Dice notation like '2d6' or '1d20'") @field_validator("notation") @classmethod def validate_notation(cls, v: str) -> str: """Validate dice notation format.""" if not isinstance(v, str): raise ValueError("Notation must be a string") # Remove spaces and convert to lowercase notation = v.strip().lower() # Validate format using regex pattern = re.compile(r"^(\d+)d(\d+)$") match = pattern.match(notation) if not match: raise ValueError( f"Invalid dice notation: '{v}'. " f"Expected format: 'XdY' (e.g., '2d6', '1d20')" ) dice_count = int(match.group(1)) sides = int(match.group(2)) # Validate reasonable limits if dice_count <= 0: raise ValueError("Dice count must be greater than 0") if dice_count > 100: raise ValueError("Dice count must not exceed 100") if sides <= 0: raise ValueError("Number of sides must be greater than 0") if sides > 1000: raise ValueError("Number of sides must not exceed 1000") return notation class DiceRollResponse(BaseModel): """Dice roll tool response.""" values: list[int] = Field(..., description="Individual dice roll results") total: int = Field(..., description="Sum of all dice rolls") notation: str = Field(..., description="Original dice notation") class WeatherRequest(BaseModel): """Weather tool request with location validation.""" location: str = Field(..., description="City name or coordinates (lat,lon)") @field_validator("location") @classmethod def validate_location(cls, v: str) -> str: """Validate location format.""" if not isinstance(v, str): raise ValueError("Location must be a string") location = v.strip() if not location: raise ValueError("Location cannot be empty") return location class WeatherResponse(BaseModel): """Weather tool response.""" location: str = Field(..., description="Requested location") temperature: float = Field(..., description="Temperature in Celsius") condition: str = Field(..., description="Weather condition description") wind_speed: float = Field(..., description="Wind speed in km/h") humidity: float | None = Field(None, description="Humidity percentage") timestamp: str | None = Field(None, description="Data timestamp") class DateTimeRequest(BaseModel): """Date/time tool request with timezone validation.""" timezone: str = Field( "UTC", description="Timezone identifier (e.g., 'UTC', 'America/New_York')" ) @field_validator("timezone") @classmethod def validate_timezone(cls, v: str) -> str: """Validate timezone format.""" if not isinstance(v, str): raise ValueError("Timezone must be a string") timezone = v.strip() if not timezone: raise ValueError("Timezone cannot be empty") return timezone class DateTimeResponse(BaseModel): """Date/time tool response.""" datetime: str = Field(..., description="ISO 8601 formatted date/time") timezone: str = Field(..., description="Timezone identifier") timestamp: float = Field(..., description="Unix timestamp") class ToolCallRequest(BaseModel): """Generic tool call request.""" name: str = Field(..., description="Tool name") arguments: dict = Field(..., description="Tool arguments") class ToolCallResponse(BaseModel): """Generic tool call response.""" content: list[dict] = Field(..., description="Tool response content") isError: bool = Field(False, description="Whether this is an error response") ================================================ FILE: examples/mcp-server-client/src/mcp_server/tools/__init__.py ================================================ """MCP server tools package.""" from .base import ( AsyncHttpMixin, BaseTool, ExternalServiceError, ToolError, ValidationToolError, ) __all__ = [ "AsyncHttpMixin", "BaseTool", "ExternalServiceError", "ToolError", "ValidationToolError", ] ================================================ FILE: examples/mcp-server-client/src/mcp_server/tools/base.py ================================================ """Base tool interface and common patterns for MCP server tools.""" import logging from abc import ABC, abstractmethod from typing import Any import httpx from pydantic import BaseModel, ValidationError logger = logging.getLogger(__name__) class ToolError(Exception): """Base exception for tool-related errors.""" def __init__( self, message: str, code: int = -1, data: dict[str, Any] | None = None ): super().__init__(message) self.message = message self.code = code self.data = data or {} class ValidationToolError(ToolError): """Exception for input validation errors.""" def __init__(self, message: str, validation_errors: list | None = None): super().__init__(message, code=-32602) # Invalid params error code self.validation_errors = validation_errors or [] class ExternalServiceError(ToolError): """Exception for external service errors.""" def __init__(self, message: str, service_name: str, status_code: int | None = None): super().__init__(message, code=-32603) # Internal error code self.service_name = service_name self.status_code = status_code class BaseTool(ABC): """Abstract base class for all MCP server tools.""" def __init__(self, name: str, description: str): self.name = name self.description = description self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") @abstractmethod async def execute(self, **kwargs: Any) -> Any: """Execute the tool with the given arguments.""" pass def validate_input( self, input_data: dict[str, Any], model_class: type[BaseModel] ) -> BaseModel: """Validate input data against a Pydantic model.""" try: return model_class(**input_data) except ValidationError as e: error_messages = [] for error in e.errors(): field = " -> ".join(str(x) for x in error["loc"]) message = error["msg"] error_messages.append(f"{field}: {message}") raise ValidationToolError( f"Invalid input for {self.name}: {'; '.join(error_messages)}", validation_errors=e.errors(), ) def create_success_response(self, data: Any) -> dict[str, Any]: """Create a successful tool response.""" return { "content": [ { "type": "text", "text": str(data) if not isinstance(data, dict | list) else data, } ], "isError": False, } def create_error_response(self, error: Exception) -> dict[str, Any]: """Create an error tool response.""" if isinstance(error, ToolError): error_message = error.message else: error_message = f"An unexpected error occurred: {str(error)}" # Log the full error for debugging self.logger.error(f"Tool {self.name} error: {error}", exc_info=True) return { "content": [ { "type": "text", "text": error_message, } ], "isError": True, } async def safe_execute(self, **kwargs) -> dict[str, Any]: """Execute the tool with error handling.""" try: result = await self.execute(**kwargs) return self.create_success_response(result) except Exception as e: return self.create_error_response(e) class AsyncHttpMixin: """Mixin for tools that need HTTP client capabilities.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._http_client = None @property def http_client(self): """Get or create HTTP client.""" if self._http_client is None: import httpx self._http_client = httpx.AsyncClient( timeout=30.0, headers={"User-Agent": "MCP-Server/1.0"}, ) return self._http_client async def cleanup(self): """Cleanup HTTP client resources.""" if self._http_client: await self._http_client.aclose() self._http_client = None async def make_request( self, method: str, url: str, timeout: float = 10.0, **kwargs ) -> dict[str, Any]: """Make an HTTP request with error handling.""" try: response = await self.http_client.request( method=method, url=url, timeout=timeout, **kwargs ) response.raise_for_status() return response.json() except httpx.TimeoutException: raise ExternalServiceError( f"Request to {url} timed out after {timeout} seconds", service_name="HTTP", ) except httpx.HTTPStatusError as e: raise ExternalServiceError( f"HTTP error {e.response.status_code}: {e.response.text}", service_name="HTTP", status_code=e.response.status_code, ) except Exception as e: raise ExternalServiceError( f"Failed to make request to {url}: {str(e)}", service_name="HTTP", ) ================================================ FILE: examples/mcp-server-client/src/mcp_server/tools/date_time.py ================================================ """Date and time tool for MCP server with timezone support.""" import zoneinfo from datetime import UTC, datetime from typing import Any from ..models import DateTimeRequest, DateTimeResponse from .base import BaseTool, ToolError class DateTimeTool(BaseTool): """Tool for getting current date and time in various timezones.""" def __init__(self): super().__init__( name="get_date", description="Get current date and time in ISO 8601 format for any timezone", ) # Common timezone aliases for user convenience self.timezone_aliases = { "utc": "UTC", "gmt": "UTC", "est": "America/New_York", "pst": "America/Los_Angeles", "cst": "America/Chicago", "mst": "America/Denver", "edt": "America/New_York", "pdt": "America/Los_Angeles", "cdt": "America/Chicago", "mdt": "America/Denver", "bst": "Europe/London", "cet": "Europe/Paris", "jst": "Asia/Tokyo", "aest": "Australia/Sydney", } def parse_timezone(self, timezone_str: str) -> zoneinfo.ZoneInfo | type[UTC]: """Parse timezone string to ZoneInfo object.""" # Normalize timezone string tz_lower = timezone_str.lower().strip() # Handle UTC as a special case if tz_lower in ("utc", "gmt"): return UTC # Check aliases if tz_lower in self.timezone_aliases: timezone_str = self.timezone_aliases[tz_lower] # Try to create ZoneInfo object try: if timezone_str.upper() == "UTC": return UTC else: return zoneinfo.ZoneInfo(timezone_str) except zoneinfo.ZoneInfoNotFoundError: # Provide helpful error message with suggestions common_timezones = [ "UTC", "America/New_York", "America/Los_Angeles", "America/Chicago", "Europe/London", "Europe/Paris", "Asia/Tokyo", "Australia/Sydney", ] suggestions = ", ".join(common_timezones) aliases = ", ".join(self.timezone_aliases.keys()) raise ToolError( f"Invalid timezone: '{timezone_str}'. " f"Common timezones: {suggestions}. " f"Aliases: {aliases}. " f"Use IANA timezone names (e.g., 'America/New_York') or aliases." ) async def execute(self, **kwargs: Any) -> DateTimeResponse: """Get current date and time for the specified timezone.""" timezone = kwargs.get("timezone", "UTC") # Validate input request = self.validate_input({"timezone": timezone}, DateTimeRequest) # Parse timezone tz = self.parse_timezone(request.timezone) self.logger.info(f"Getting current time for timezone: {timezone}") # Get current time in the specified timezone if isinstance(tz, zoneinfo.ZoneInfo): current_time = datetime.now(tz) tz_name = str(tz) else: # UTC current_time = datetime.now(tz) tz_name = "UTC" # Format as ISO 8601 iso_datetime = current_time.isoformat() # Get Unix timestamp timestamp = current_time.timestamp() self.logger.info(f"Current time in {tz_name}: {iso_datetime}") return DateTimeResponse( datetime=iso_datetime, timezone=tz_name, timestamp=timestamp, ) def format_result(self, response: DateTimeResponse) -> str: """Format date/time data for display.""" # Parse the ISO datetime to extract components try: dt = datetime.fromisoformat(response.datetime) date_part = dt.strftime("%Y-%m-%d") time_part = dt.strftime("%H:%M:%S") weekday = dt.strftime("%A") result = "🕐 **Current Date & Time**\n" result += f"📅 Date: **{date_part}** ({weekday})\n" result += f"⏰ Time: **{time_part}**\n" result += f"🌍 Timezone: **{response.timezone}**\n" result += f"📋 ISO 8601: `{response.datetime}`\n" result += f"🔢 Unix Timestamp: `{int(response.timestamp)}`" return result except ValueError: # Fallback if datetime parsing fails return ( f"🕐 **Current Date & Time**\n" f"📋 ISO 8601: `{response.datetime}`\n" f"🌍 Timezone: **{response.timezone}**\n" f"🔢 Unix Timestamp: `{int(response.timestamp)}`" ) async def safe_execute(self, **kwargs) -> dict[str, Any]: """Execute date/time lookup with formatted output.""" try: result = await self.execute(**kwargs) formatted_result = self.format_result(result) return { "content": [ { "type": "text", "text": formatted_result, } ], "isError": False, } except Exception as e: return self.create_error_response(e) def get_available_timezones(self) -> list[str]: """Get a list of common available timezones.""" common_zones = [ "UTC", "America/New_York", "America/Los_Angeles", "America/Chicago", "America/Denver", "America/Phoenix", "America/Anchorage", "America/Honolulu", "America/Toronto", "America/Vancouver", "Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Rome", "Europe/Madrid", "Europe/Amsterdam", "Europe/Stockholm", "Europe/Moscow", "Asia/Tokyo", "Asia/Shanghai", "Asia/Kolkata", "Asia/Dubai", "Asia/Singapore", "Australia/Sydney", "Australia/Melbourne", "Pacific/Auckland", ] return sorted(common_zones + list(self.timezone_aliases.values())) ================================================ FILE: examples/mcp-server-client/src/mcp_server/tools/dice.py ================================================ """Dice rolling tool for MCP server.""" import random import re from typing import Any from ..models import DiceRollRequest, DiceRollResponse from .base import BaseTool, ToolError class DiceRollTool(BaseTool): """Tool for rolling dice using standard notation.""" def __init__(self): super().__init__( name="roll_dice", description="Roll dice using standard notation like '2d6' or '1d20'", ) self.notation_pattern = re.compile(r"^(\d+)d(\d+)$") async def execute(self, **kwargs: Any) -> DiceRollResponse: """Execute dice roll with the given notation.""" notation = kwargs.get("notation") if not notation: raise ToolError("Missing required parameter: notation") # Validate input using Pydantic model request = self.validate_input({"notation": notation}, DiceRollRequest) # Parse the notation match = self.notation_pattern.match(request.notation) if not match: raise ToolError(f"Invalid dice notation: {notation}") dice_count = int(match.group(1)) sides = int(match.group(2)) self.logger.info(f"Rolling {dice_count}d{sides}") # Generate random values for each die values = [] for _ in range(dice_count): roll = random.randint(1, sides) values.append(roll) total = sum(values) self.logger.info(f"Dice roll result: {values} (total: {total})") # Return structured response return DiceRollResponse( values=values, total=total, notation=str(notation), # Return original notation as provided ) def format_result(self, response: DiceRollResponse) -> str: """Format dice roll result for display.""" if len(response.values) == 1: return f"🎲 Rolled {response.notation}: **{response.values[0]}**" else: values_str = ", ".join(map(str, response.values)) return ( f"🎲 Rolled {response.notation}: [{values_str}] = **{response.total}**" ) async def safe_execute(self, **kwargs) -> dict[str, Any]: """Execute dice roll with formatted output.""" try: result = await self.execute(**kwargs) formatted_result = self.format_result(result) return { "content": [ { "type": "text", "text": formatted_result, } ], "isError": False, } except Exception as e: return self.create_error_response(e) ================================================ FILE: examples/mcp-server-client/src/mcp_server/tools/weather.py ================================================ """Weather tool for MCP server using Open-Meteo API.""" from datetime import datetime from typing import Any from ..models import WeatherRequest, WeatherResponse from .base import AsyncHttpMixin, BaseTool, ExternalServiceError, ToolError class WeatherTool(BaseTool, AsyncHttpMixin): """Tool for getting current weather data.""" def __init__(self): super().__init__( name="get_weather", description="Get current weather conditions for a location", ) self.api_base = "https://api.open-meteo.com/v1" # Basic city to coordinates mapping # In production, this would use a proper geocoding service self.city_coords = { "san francisco": (37.7749, -122.4194), "new york": (40.7128, -74.0060), "london": (51.5074, -0.1278), "paris": (48.8566, 2.3522), "tokyo": (35.6762, 139.6503), "sydney": (-33.8688, 151.2093), "los angeles": (34.0522, -118.2437), "chicago": (41.8781, -87.6298), "miami": (25.7617, -80.1918), "seattle": (47.6062, -122.3321), "vancouver": (49.2827, -123.1207), "toronto": (43.6532, -79.3832), "berlin": (52.5200, 13.4050), "rome": (41.9028, 12.4964), "madrid": (40.4168, -3.7038), "moscow": (55.7558, 37.6176), "beijing": (39.9042, 116.4074), "mumbai": (19.0760, 72.8777), "cairo": (30.0444, 31.2357), "lagos": (6.5244, 3.3792), } # Weather code to description mapping (subset of WMO codes) self.weather_codes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", } def parse_location(self, location: str) -> tuple[float, float]: """Parse location string to get coordinates.""" location_lower = location.lower().strip() # Check if it's in our city mapping if location_lower in self.city_coords: return self.city_coords[location_lower] # Try to parse as "lat,lon" coordinates try: parts = location.split(",") if len(parts) == 2: lat = float(parts[0].strip()) lon = float(parts[1].strip()) # Basic validation for reasonable coordinate ranges if -90 <= lat <= 90 and -180 <= lon <= 180: return lat, lon except ValueError: pass # If we can't parse the location, raise an error available_cities = ", ".join(sorted(self.city_coords.keys())) raise ToolError( f"Unknown location: '{location}'. " f"Please use coordinates (lat,lon) or one of: {available_cities}" ) def weather_code_to_text(self, code: int) -> str: """Convert weather code to readable description.""" return self.weather_codes.get(code, f"Unknown weather condition (code: {code})") async def execute(self, **kwargs: Any) -> WeatherResponse: """Get weather data for the specified location.""" location = kwargs.get("location") if not location: raise ToolError("Missing required parameter: location") # Validate input request = self.validate_input({"location": location}, WeatherRequest) # Parse location to coordinates lat, lon = self.parse_location(request.location) self.logger.info(f"Getting weather for {location} ({lat}, {lon})") # Make API request to Open-Meteo try: data = await self.make_request( method="GET", url=f"{self.api_base}/forecast", params={ "latitude": lat, "longitude": lon, "current": ( "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m" ), "timezone": "auto", }, timeout=15.0, ) # Extract current weather data current = data.get("current", {}) if not current: raise ExternalServiceError( "No current weather data available", service_name="Open-Meteo", ) temperature = current.get("temperature_2m") humidity = current.get("relative_humidity_2m") weather_code = current.get("weather_code") wind_speed = current.get("wind_speed_10m") timestamp = current.get("time") if temperature is None or weather_code is None or wind_speed is None: raise ExternalServiceError( "Incomplete weather data received", service_name="Open-Meteo", ) condition = self.weather_code_to_text(weather_code) self.logger.info( f"Weather data retrieved: {temperature}°C, {condition}, " f"{wind_speed} km/h wind" ) return WeatherResponse( location=str(location), temperature=temperature, condition=condition, wind_speed=wind_speed, humidity=humidity, timestamp=timestamp, ) except ExternalServiceError: raise except Exception as e: raise ExternalServiceError( f"Failed to retrieve weather data: {str(e)}", service_name="Open-Meteo", ) def format_result(self, response: WeatherResponse) -> str: """Format weather data for display.""" result = f"🌤️ **Weather for {response.location}**\n" result += f"🌡️ Temperature: **{response.temperature}°C**\n" result += f"☁️ Condition: **{response.condition}**\n" result += f"💨 Wind Speed: **{response.wind_speed} km/h**" if response.humidity is not None: result += f"\n💧 Humidity: **{response.humidity}%**" if response.timestamp: try: # Parse and format timestamp dt = datetime.fromisoformat(response.timestamp.replace("Z", "+00:00")) result += f"\n🕐 Updated: {dt.strftime('%Y-%m-%d %H:%M UTC')}" except ValueError: pass return result async def safe_execute(self, **kwargs) -> dict[str, Any]: """Execute weather lookup with formatted output.""" try: result = await self.execute(**kwargs) formatted_result = self.format_result(result) return { "content": [ { "type": "text", "text": formatted_result, } ], "isError": False, } except Exception as e: return self.create_error_response(e) ================================================ FILE: examples/mcp-server-client/tests/__init__.py ================================================ ================================================ FILE: examples/mcp-server-client/tests/test_cli.py ================================================ """Tests for MCP client CLI interface.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.mcp_client.cli import MCPClientCLI from src.mcp_client.models.responses import ClientToolResult class TestMCPClientCLI: """Test cases for MCPClientCLI class.""" def test_init(self): """Test CLI initialization.""" cli = MCPClientCLI() assert cli.parser is not None assert cli.client is None def test_parser_creation(self): """Test argument parser creation.""" cli = MCPClientCLI() parser = cli.parser # Test that parser exists and has expected subcommands assert parser is not None # Test help doesn't crash help_text = parser.format_help() assert "MCP Client" in help_text assert "--server" in help_text def test_build_tool_arguments_roll_dice(self): """Test building arguments for roll_dice tool.""" cli = MCPClientCLI() # Mock parsed arguments args = MagicMock() args.tool = "roll_dice" args.notation = "2d6" result = cli._build_tool_arguments(args) assert result == {"notation": "2d6"} def test_build_tool_arguments_get_weather(self): """Test building arguments for get_weather tool.""" cli = MCPClientCLI() # Mock parsed arguments args = MagicMock() args.tool = "get_weather" args.location = "San Francisco" result = cli._build_tool_arguments(args) assert result == {"location": "San Francisco"} def test_build_tool_arguments_get_date(self): """Test building arguments for get_date tool.""" cli = MCPClientCLI() # Mock parsed arguments args = MagicMock() args.tool = "get_date" args.timezone = "UTC" result = cli._build_tool_arguments(args) assert result == {"timezone": "UTC"} def test_build_tool_arguments_unknown(self): """Test building arguments for unknown tool.""" cli = MCPClientCLI() # Mock parsed arguments args = MagicMock() args.tool = "unknown_tool" result = cli._build_tool_arguments(args) assert result == {} def test_parse_roll_dice_arguments(self): """Test parsing roll_dice command arguments.""" cli = MCPClientCLI() args = cli.parser.parse_args( ["--server", "test_server.py", "roll_dice", "--notation", "3d6"] ) assert args.server == "test_server.py" assert args.tool == "roll_dice" assert args.notation == "3d6" assert args.log_level == "INFO" # default assert args.timeout == 30 # default def test_parse_get_weather_arguments(self): """Test parsing get_weather command arguments.""" cli = MCPClientCLI() args = cli.parser.parse_args( [ "--server", "test_server.py", "--log-level", "DEBUG", "get_weather", "--location", "London", ] ) assert args.server == "test_server.py" assert args.tool == "get_weather" assert args.location == "London" assert args.log_level == "DEBUG" def test_parse_get_date_arguments(self): """Test parsing get_date command arguments.""" cli = MCPClientCLI() args = cli.parser.parse_args( [ "--server", "test_server.py", "--timeout", "60", "get_date", "--timezone", "America/New_York", ] ) assert args.server == "test_server.py" assert args.tool == "get_date" assert args.timezone == "America/New_York" assert args.timeout == 60 def test_parse_missing_server(self): """Test parsing with missing required --server argument.""" cli = MCPClientCLI() with pytest.raises(SystemExit): cli.parser.parse_args(["roll_dice", "--notation", "2d6"]) def test_parse_missing_tool_arguments(self): """Test parsing with missing required tool arguments.""" cli = MCPClientCLI() with pytest.raises(SystemExit): cli.parser.parse_args(["--server", "test.py", "roll_dice"]) @pytest.mark.asyncio async def test_run_no_tool_specified(self, capsys): """Test running CLI without specifying a tool.""" cli = MCPClientCLI() exit_code = await cli.run(["--server", "test_server.py"]) assert exit_code == 1 captured = capsys.readouterr() assert "usage:" in captured.out @pytest.mark.asyncio async def test_run_connection_timeout(self): """Test running CLI with connection timeout.""" cli = MCPClientCLI() with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.side_effect = TimeoutError() exit_code = await cli.run( [ "--server", "test_server.py", "--timeout", "1", "roll_dice", "--notation", "2d6", ] ) assert exit_code == 1 @pytest.mark.asyncio async def test_run_connection_error(self): """Test running CLI with connection error.""" cli = MCPClientCLI() with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.side_effect = ConnectionError("Server not found") exit_code = await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) assert exit_code == 1 @pytest.mark.asyncio async def test_run_successful_tool_execution(self): """Test running CLI with successful tool execution.""" cli = MCPClientCLI() # Mock successful result mock_result = ClientToolResult( success=True, result=MagicMock(content=[{"text": "🎲 Rolled 2d6: [3, 5] = **8**"}]), tool_name="roll_dice", arguments={"notation": "2d6"}, ) with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.return_value = None mock_client.invoke_tool.return_value = mock_result mock_client.disconnect.return_value = None exit_code = await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) assert exit_code == 0 mock_client.connect.assert_called_once() mock_client.invoke_tool.assert_called_once_with( "roll_dice", {"notation": "2d6"} ) mock_client.disconnect.assert_called_once() @pytest.mark.asyncio async def test_run_failed_tool_execution(self): """Test running CLI with failed tool execution.""" cli = MCPClientCLI() # Mock failed result mock_result = ClientToolResult( success=False, error="Tool execution failed", tool_name="roll_dice", arguments={"notation": "invalid"}, ) with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.return_value = None mock_client.invoke_tool.return_value = mock_result mock_client.disconnect.return_value = None exit_code = await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "invalid"] ) assert exit_code == 1 mock_client.invoke_tool.assert_called_once_with( "roll_dice", {"notation": "invalid"} ) @pytest.mark.asyncio async def test_run_keyboard_interrupt(self): """Test running CLI with keyboard interrupt.""" cli = MCPClientCLI() with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.side_effect = KeyboardInterrupt() exit_code = await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) assert exit_code == 130 @pytest.mark.asyncio async def test_run_unexpected_error(self): """Test running CLI with unexpected error.""" cli = MCPClientCLI() with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.side_effect = RuntimeError("Unexpected error") exit_code = await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) assert exit_code == 1 @pytest.mark.asyncio async def test_run_cleanup_on_success(self): """Test that client is properly cleaned up on success.""" cli = MCPClientCLI() mock_result = ClientToolResult( success=True, result=MagicMock(content=[{"text": "Success"}]), tool_name="roll_dice", arguments={"notation": "2d6"}, ) with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.return_value = None mock_client.invoke_tool.return_value = mock_result mock_client.disconnect.return_value = None await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) # Verify cleanup was called mock_client.disconnect.assert_called_once() @pytest.mark.asyncio async def test_run_cleanup_on_error(self): """Test that client is properly cleaned up on error.""" cli = MCPClientCLI() with patch("src.mcp_client.cli.MCPClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value = mock_client mock_client.connect.return_value = None mock_client.invoke_tool.side_effect = Exception("Test error") mock_client.disconnect.return_value = None await cli.run( ["--server", "test_server.py", "roll_dice", "--notation", "2d6"] ) # Verify cleanup was called even on error mock_client.disconnect.assert_called_once() def test_display_success_with_text_content(self, capsys): """Test displaying successful result with text content.""" cli = MCPClientCLI() result = ClientToolResult( success=True, result=MagicMock(content=[{"text": "🎲 Rolled 2d6: [3, 5] = **8**"}]), tool_name="roll_dice", arguments={"notation": "2d6"}, ) cli._display_success(result) captured = capsys.readouterr() assert "✅ roll_dice executed successfully" in captured.out assert "🎲 Rolled 2d6: [3, 5] = **8**" in captured.out def test_display_success_with_json_content(self, capsys): """Test displaying successful result with JSON content.""" cli = MCPClientCLI() result = ClientToolResult( success=True, result=MagicMock(content=[{"data": "test", "value": 42}]), tool_name="test_tool", arguments={}, ) cli._display_success(result) captured = capsys.readouterr() assert "✅ test_tool executed successfully" in captured.out assert '"data": "test"' in captured.out assert '"value": 42' in captured.out def test_display_error(self, capsys): """Test displaying error result.""" cli = MCPClientCLI() result = ClientToolResult( success=False, error="Tool execution failed", tool_name="roll_dice", arguments={"notation": "invalid"}, ) cli._display_error(result) captured = capsys.readouterr() assert "❌ roll_dice failed" in captured.out assert "Error: Tool execution failed" in captured.out assert "Tool: roll_dice" in captured.out assert '"notation": "invalid"' in captured.out def test_display_connection_error(self, capsys): """Test displaying connection error.""" cli = MCPClientCLI() error = ConnectionError("Server not found") cli._display_connection_error(error) captured = capsys.readouterr() assert "❌ Connection failed" in captured.out assert "Error: Server not found" in captured.out assert "Troubleshooting:" in captured.out assert "Check that the server script exists" in captured.out @pytest.mark.asyncio async def test_main_function(): """Test main function.""" with patch("src.mcp_client.cli.MCPClientCLI") as mock_cli_class: mock_cli = AsyncMock() mock_cli_class.return_value = mock_cli mock_cli.run.return_value = 0 from src.mcp_client.cli import main with patch( "sys.argv", ["cli.py", "--server", "test.py", "roll_dice", "--notation", "2d6"], ): result = await main() assert result == 0 mock_cli.run.assert_called_once() ================================================ FILE: examples/mcp-server-client/tests/test_gui.py ================================================ """Tests for GUI components and functionality.""" from datetime import datetime from unittest.mock import Mock, patch import pytest from src.gui.models.gui_models import ConnectionStatus, GUIInteraction, GUISession from src.gui.utils.formatting import ( format_error_message, format_execution_time, format_json_for_display, ) from src.gui.utils.validation import ( validate_dice_notation, validate_location, validate_timezone, ) class TestGUIModels: """Test GUI data models.""" def test_gui_session_model(self): """Test GUI session model validation.""" session = GUISession() assert not session.connected assert session.server_path == "src/mcp_server/server.py" assert len(session.interaction_history) == 0 assert session.current_tool is None def test_gui_interaction_model(self): """Test GUI interaction model.""" interaction = GUIInteraction( tool_name="roll_dice", arguments={"notation": "2d6"}, request_payload={"tool": "roll_dice", "arguments": {"notation": "2d6"}}, response_payload={ "success": True, "result": {"values": [3, 5], "total": 8}, }, success=True, ) assert interaction.tool_name == "roll_dice" assert interaction.success is True assert interaction.timestamp is not None assert isinstance(interaction.timestamp, datetime) def test_connection_status_model(self): """Test connection status model.""" status = ConnectionStatus( server_path="src/mcp_server/server.py", connected=True, available_tools=["roll_dice", "get_weather"], ) assert status.connected is True assert len(status.available_tools) == 2 assert "roll_dice" in status.available_tools class TestValidationUtils: """Test validation utilities.""" def test_dice_notation_validation(self): """Test dice notation validation.""" # Valid notations assert validate_dice_notation("2d6") is True assert validate_dice_notation("1d20") is True assert validate_dice_notation("10d10") is True # Invalid notations assert validate_dice_notation("invalid") is False assert validate_dice_notation("2d") is False assert validate_dice_notation("d6") is False assert validate_dice_notation("") is False assert validate_dice_notation(None) is False # Edge cases assert validate_dice_notation("0d6") is False # No dice assert validate_dice_notation("2d1") is False # Invalid sides assert validate_dice_notation("101d6") is False # Too many dice def test_location_validation(self): """Test location validation.""" # Valid locations assert validate_location("San Francisco") is True assert validate_location("New York") is True assert validate_location("37.7749,-122.4194") is True assert validate_location("40.7128,-74.0060") is True # Invalid locations assert validate_location("") is False assert validate_location("A") is False # Too short assert validate_location("123") is False # Numbers only assert validate_location(None) is False def test_timezone_validation(self): """Test timezone validation.""" # Valid timezones assert validate_timezone("UTC") is True assert validate_timezone("America/New_York") is True assert validate_timezone("Europe/London") is True assert validate_timezone("Asia/Tokyo") is True # Invalid timezones assert validate_timezone("") is False assert validate_timezone("Invalid/Zone") is True # Would pass pattern check assert validate_timezone("123") is False assert validate_timezone(None) is False class TestFormattingUtils: """Test formatting utilities.""" def test_json_formatting(self): """Test JSON formatting for display.""" data = {"tool": "roll_dice", "result": {"values": [3, 5], "total": 8}} formatted = format_json_for_display(data) assert isinstance(formatted, str) assert "roll_dice" in formatted assert "values" in formatted # Test with invalid data formatted_error = format_json_for_display( set() ) # Sets aren't JSON serializable assert "Error formatting JSON" in formatted_error def test_error_message_formatting(self): """Test error message formatting.""" # Test removal of prefixes assert ( format_error_message("Exception: Something went wrong") == "Something went wrong" ) assert format_error_message("Error: Invalid input") == "Invalid input" # Test capitalization assert format_error_message("invalid dice notation") == "Invalid dice notation" # Test empty string assert format_error_message("") == "" def test_execution_time_formatting(self): """Test execution time formatting.""" # Test milliseconds assert format_execution_time(0.5) == "500ms" assert format_execution_time(0.123) == "123ms" # Test seconds assert format_execution_time(1.5) == "1.50s" assert format_execution_time(30.25) == "30.25s" # Test minutes assert format_execution_time(65.5) == "1m 5.5s" assert format_execution_time(125.75) == "2m 5.8s" class TestConnectionManager: """Test connection manager component.""" @patch("src.gui.components.connection.MCPClient") def test_connection_success(self, mock_client_class): """Test successful connection.""" # Mock client instance mock_client = Mock() mock_client.available_tools = ["roll_dice", "get_weather", "get_date"] mock_client_class.return_value = mock_client # This would require mocking Streamlit session state # which is complex, so we're testing the basic structure assert mock_client_class is not None def test_connection_failure(self): """Test connection failure handling.""" # This would test error handling in connection # Placeholder for more complex Streamlit testing pass class TestToolForms: """Test tool form components.""" def test_dice_form_validation(self): """Test dice form validation logic.""" from src.gui.components.tool_forms import ToolForms tool_forms = ToolForms() assert tool_forms._validate_dice_notation("2d6") is True assert tool_forms._validate_dice_notation("invalid") is False def test_tool_execution_flow(self): """Test tool execution flow.""" # This would test the complete flow from form submission # to result display - requires Streamlit mocking pass @pytest.mark.asyncio async def test_async_operations(): """Test async operations in GUI components.""" # Placeholder for testing async MCP client operations # This would test the asyncio.run() calls in the GUI pass def test_session_state_management(): """Test session state management patterns.""" # Test session initialization session = GUISession() assert session.connected is False assert len(session.interaction_history) == 0 # Test adding interactions interaction = GUIInteraction( tool_name="test_tool", arguments={"test": "value"}, request_payload={"tool": "test_tool"}, response_payload={"success": True}, success=True, ) session.interaction_history.append(interaction) assert len(session.interaction_history) == 1 assert session.interaction_history[0].tool_name == "test_tool" def test_history_management(): """Test history management functionality.""" # Create test interactions interactions = [] for i in range(3): interaction = GUIInteraction( tool_name=f"tool_{i}", arguments={"index": i}, request_payload={"tool": f"tool_{i}"}, response_payload={"success": True, "index": i}, success=True, ) interactions.append(interaction) # Test history ordering (latest first) assert interactions[-1].arguments["index"] == 2 assert interactions[0].arguments["index"] == 0 ================================================ FILE: examples/mcp-server-client/tests/test_mcp_client.py ================================================ """Tests for MCP client functionality.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.mcp_client.client import MCPClient from src.mcp_client.models.responses import ClientToolResult from src.mcp_client.transport import MCPTransport class TestMCPTransport: """Test cases for MCPTransport class.""" def test_init(self): """Test transport initialization.""" transport = MCPTransport("test_server.py") assert transport.server_path == "test_server.py" assert transport.session is None assert transport.connected is False assert transport.available_tools == [] @pytest.mark.asyncio async def test_connect_file_not_found(self): """Test connection with non-existent server file.""" transport = MCPTransport("nonexistent_server.py") with pytest.raises(FileNotFoundError, match="Server script not found"): await transport.connect() @pytest.mark.asyncio async def test_connect_unsupported_file_type(self, tmp_path): """Test connection with unsupported file type.""" server_file = tmp_path / "server.txt" server_file.write_text("# not a valid server script") transport = MCPTransport(str(server_file)) with pytest.raises(ValueError, match="Unsupported server script type"): await transport.connect() @pytest.mark.asyncio async def test_connect_success(self, tmp_path): """Test successful connection.""" server_file = tmp_path / "server.py" server_file.write_text("# mock server script") transport = MCPTransport(str(server_file)) # Mock the connection components with ( patch("src.mcp_client.transport.stdio_client") as mock_stdio, patch("src.mcp_client.transport.ClientSession") as mock_session_class, ): # Setup mocks mock_read, mock_write = AsyncMock(), AsyncMock() mock_stdio.return_value.__aenter__.return_value = (mock_read, mock_write) mock_session = AsyncMock() mock_session_class.return_value.__aenter__.return_value = mock_session # Mock session methods mock_session.initialize = AsyncMock() mock_tools_response = MagicMock() mock_tool1 = MagicMock() mock_tool1.name = "roll_dice" mock_tool2 = MagicMock() mock_tool2.name = "get_weather" mock_tool3 = MagicMock() mock_tool3.name = "get_date" mock_tools_response.tools = [mock_tool1, mock_tool2, mock_tool3] mock_session.list_tools.return_value = mock_tools_response # Test connection await transport.connect() # Verify connection state assert transport.connected is True assert transport.available_tools == ["roll_dice", "get_weather", "get_date"] assert transport.session == mock_session @pytest.mark.asyncio async def test_disconnect(self): """Test disconnect functionality.""" transport = MCPTransport("test_server.py") # Mock exit stack mock_exit_stack = AsyncMock() transport.exit_stack = mock_exit_stack transport.connected = True transport.available_tools = ["test_tool"] await transport.disconnect() mock_exit_stack.aclose.assert_called_once() assert transport.session is None assert transport.connected is False assert transport.available_tools == [] @pytest.mark.asyncio async def test_call_tool_not_connected(self): """Test tool call when not connected.""" transport = MCPTransport("test_server.py") with pytest.raises(RuntimeError, match="Not connected to server"): await transport.call_tool("test_tool", {}) @pytest.mark.asyncio async def test_call_tool_unavailable(self): """Test tool call with unavailable tool.""" transport = MCPTransport("test_server.py") transport.connected = True transport.session = AsyncMock() # Add session mock transport.available_tools = ["available_tool"] with pytest.raises(ValueError, match="Tool 'unavailable_tool' not available"): await transport.call_tool("unavailable_tool", {}) @pytest.mark.asyncio async def test_call_tool_success(self): """Test successful tool call.""" transport = MCPTransport("test_server.py") transport.connected = True transport.available_tools = ["test_tool"] # Mock session mock_session = AsyncMock() transport.session = mock_session mock_result = MagicMock() mock_session.call_tool.return_value = mock_result result = await transport.call_tool("test_tool", {"arg": "value"}) mock_session.call_tool.assert_called_once_with("test_tool", {"arg": "value"}) assert result == mock_result @pytest.mark.asyncio async def test_health_check_not_connected(self): """Test health check when not connected.""" transport = MCPTransport("test_server.py") result = await transport.health_check() assert result is False @pytest.mark.asyncio async def test_health_check_success(self): """Test successful health check.""" transport = MCPTransport("test_server.py") transport.connected = True mock_session = AsyncMock() transport.session = mock_session mock_session.list_tools.return_value = MagicMock() result = await transport.health_check() assert result is True @pytest.mark.asyncio async def test_health_check_failure(self): """Test health check with exception.""" transport = MCPTransport("test_server.py") transport.connected = True mock_session = AsyncMock() transport.session = mock_session mock_session.list_tools.side_effect = Exception("Connection lost") result = await transport.health_check() assert result is False class TestMCPClient: """Test cases for MCPClient class.""" def test_init(self): """Test client initialization.""" client = MCPClient("test_server.py") assert client.server_path == "test_server.py" assert isinstance(client.transport, MCPTransport) assert client.connected is False @pytest.mark.asyncio async def test_connect_success(self): """Test successful connection.""" client = MCPClient("test_server.py") with patch.object( client.transport, "connect", new_callable=AsyncMock ) as mock_connect: mock_connect.return_value = None client.transport.connected = True client.transport.available_tools = ["test_tool"] await client.connect() mock_connect.assert_called_once() assert client.connected is True @pytest.mark.asyncio async def test_connect_failure(self): """Test connection failure.""" client = MCPClient("test_server.py") with patch.object( client.transport, "connect", new_callable=AsyncMock ) as mock_connect: mock_connect.side_effect = ConnectionError("Failed to connect") with pytest.raises(ConnectionError, match="Failed to connect"): await client.connect() @pytest.mark.asyncio async def test_disconnect(self): """Test disconnect functionality.""" client = MCPClient("test_server.py") client._connected = True with patch.object( client.transport, "disconnect", new_callable=AsyncMock ) as mock_disconnect: await client.disconnect() mock_disconnect.assert_called_once() assert client.connected is False @pytest.mark.asyncio async def test_invoke_tool_not_connected(self): """Test tool invocation when not connected.""" client = MCPClient("test_server.py") result = await client.invoke_tool("test_tool", {}) assert result.success is False assert "Not connected to server" in result.error assert result.tool_name == "test_tool" @pytest.mark.asyncio async def test_invoke_tool_unavailable(self): """Test tool invocation with unavailable tool.""" client = MCPClient("test_server.py") client._connected = True client.transport.connected = True client.transport.available_tools = ["available_tool"] result = await client.invoke_tool("unavailable_tool", {}) assert result.success is False assert "not available" in result.error assert result.tool_name == "unavailable_tool" @pytest.mark.asyncio async def test_invoke_tool_success(self): """Test successful tool invocation.""" client = MCPClient("test_server.py") client._connected = True client.transport.connected = True client.transport.available_tools = ["test_tool"] mock_result = MagicMock() mock_result.content = [{"type": "text", "text": "Success"}] with patch.object( client.transport, "call_tool", new_callable=AsyncMock ) as mock_call: mock_call.return_value = mock_result result = await client.invoke_tool("test_tool", {"arg": "value"}) mock_call.assert_called_once_with("test_tool", {"arg": "value"}) assert result.success is True assert result.result == mock_result assert result.tool_name == "test_tool" @pytest.mark.asyncio async def test_invoke_tool_exception(self): """Test tool invocation with exception.""" client = MCPClient("test_server.py") client._connected = True client.transport.connected = True client.transport.available_tools = ["test_tool"] with patch.object( client.transport, "call_tool", new_callable=AsyncMock ) as mock_call: mock_call.side_effect = Exception("Tool execution failed") result = await client.invoke_tool("test_tool", {"arg": "value"}) assert result.success is False assert "Tool execution failed" in result.error assert result.tool_name == "test_tool" @pytest.mark.asyncio async def test_health_check_not_connected(self): """Test health check when not connected.""" client = MCPClient("test_server.py") result = await client.health_check() assert result is False @pytest.mark.asyncio async def test_health_check_success(self): """Test successful health check.""" client = MCPClient("test_server.py") client._connected = True client.transport.connected = True # Also set transport connected with patch.object( client.transport, "health_check", new_callable=AsyncMock ) as mock_health: mock_health.return_value = True result = await client.health_check() assert result is True @pytest.mark.asyncio async def test_context_manager(self): """Test client as async context manager.""" client = MCPClient("test_server.py") with ( patch.object(client, "connect", new_callable=AsyncMock) as mock_connect, patch.object( client, "disconnect", new_callable=AsyncMock ) as mock_disconnect, ): async with client as context_client: assert context_client == client mock_connect.assert_called_once() mock_disconnect.assert_called_once() class TestClientToolResult: """Test cases for ClientToolResult model.""" def test_successful_result(self): """Test successful tool result.""" result = ClientToolResult( success=True, result={"data": "test"}, tool_name="test_tool", arguments={"arg": "value"}, ) assert result.success is True assert result.result == {"data": "test"} assert result.error is None assert result.tool_name == "test_tool" assert result.arguments == {"arg": "value"} def test_failed_result(self): """Test failed tool result.""" result = ClientToolResult( success=False, error="Tool execution failed", tool_name="test_tool", arguments={"arg": "value"}, ) assert result.success is False assert result.result is None assert result.error == "Tool execution failed" assert result.tool_name == "test_tool" assert result.arguments == {"arg": "value"} ================================================ FILE: examples/mcp-server-client/tests/test_mcp_server.py ================================================ """Tests for the MCP server integration.""" from unittest.mock import AsyncMock, patch import pytest from src.mcp_server.server import cleanup_server, datetime_tool, dice_tool, weather_tool from tests.fixtures.mcp_messages import WeatherAPIFixtures class TestMCPServerIntegration: """Test suite for MCP server integration.""" @pytest.mark.asyncio async def test_dice_tool_integration(self): """Test dice tool integration through MCP server.""" # Import the MCP tool function from src.mcp_server.server import roll_dice result = await roll_dice(notation="2d6") assert "content" in result assert "isError" in result assert result["isError"] is False assert "🎲" in result["content"][0]["text"] assert "2d6" in result["content"][0]["text"] @pytest.mark.asyncio async def test_weather_tool_integration(self): """Test weather tool integration through MCP server.""" from src.mcp_server.server import get_weather mock_response = WeatherAPIFixtures.current_weather_response() with patch.object(weather_tool, "make_request", return_value=mock_response): result = await get_weather(location="San Francisco") assert "content" in result assert "isError" in result assert result["isError"] is False assert "🌤️" in result["content"][0]["text"] assert "San Francisco" in result["content"][0]["text"] @pytest.mark.asyncio async def test_datetime_tool_integration(self): """Test date/time tool integration through MCP server.""" from src.mcp_server.server import get_date result = await get_date(timezone="UTC") assert "content" in result assert "isError" in result assert result["isError"] is False assert "🕐" in result["content"][0]["text"] assert "UTC" in result["content"][0]["text"] @pytest.mark.asyncio async def test_dice_tool_error_handling(self): """Test error handling in dice tool integration.""" from src.mcp_server.server import roll_dice result = await roll_dice(notation="invalid") assert "content" in result assert "isError" in result assert result["isError"] is True assert "Invalid input" in result["content"][0]["text"] @pytest.mark.asyncio async def test_weather_tool_error_handling(self): """Test error handling in weather tool integration.""" from src.mcp_server.server import get_weather result = await get_weather(location="Unknown City") assert "content" in result assert "isError" in result assert result["isError"] is True assert "Unknown location" in result["content"][0]["text"] @pytest.mark.asyncio async def test_datetime_tool_error_handling(self): """Test error handling in date/time tool integration.""" from src.mcp_server.server import get_date result = await get_date(timezone="Invalid/Timezone") assert "content" in result assert "isError" in result assert result["isError"] is True assert "Invalid timezone" in result["content"][0]["text"] @pytest.mark.asyncio async def test_help_resource(self): """Test help resource is available.""" from src.mcp_server.server import get_help help_text = await get_help() assert isinstance(help_text, str) assert "roll_dice" in help_text assert "get_weather" in help_text assert "get_date" in help_text assert "🎲" in help_text @pytest.mark.asyncio async def test_cleanup_server(self): """Test server cleanup functionality.""" # Mock the weather tool's cleanup method weather_tool.cleanup = AsyncMock() await cleanup_server() weather_tool.cleanup.assert_called_once() @pytest.mark.asyncio async def test_cleanup_server_with_error(self): """Test server cleanup handles errors gracefully.""" # Mock the weather tool's cleanup method to raise an error weather_tool.cleanup = AsyncMock(side_effect=Exception("Cleanup error")) # Should not raise an exception await cleanup_server() weather_tool.cleanup.assert_called_once() def test_tool_instances_exist(self): """Test that tool instances are properly created.""" assert dice_tool is not None assert weather_tool is not None assert datetime_tool is not None assert dice_tool.name == "roll_dice" assert weather_tool.name == "get_weather" assert datetime_tool.name == "get_date" @pytest.mark.asyncio async def test_mcp_tool_docstrings(self): """Test that MCP tool functions have proper docstrings.""" from src.mcp_server.server import get_date, get_weather, roll_dice assert roll_dice.__doc__ is not None assert "dice" in roll_dice.__doc__.lower() assert "notation" in roll_dice.__doc__.lower() assert get_weather.__doc__ is not None assert "weather" in get_weather.__doc__.lower() assert "location" in get_weather.__doc__.lower() assert get_date.__doc__ is not None assert "date" in get_date.__doc__.lower() assert "timezone" in get_date.__doc__.lower() @pytest.mark.asyncio async def test_all_tools_return_proper_format(self): """Test that all tools return the expected MCP response format.""" from src.mcp_server.server import get_date, roll_dice # Test dice tool dice_result = await roll_dice(notation="1d6") assert "content" in dice_result assert "isError" in dice_result assert isinstance(dice_result["content"], list) assert len(dice_result["content"]) == 1 assert "type" in dice_result["content"][0] assert "text" in dice_result["content"][0] # Test date tool date_result = await get_date(timezone="UTC") assert "content" in date_result assert "isError" in date_result assert isinstance(date_result["content"], list) assert len(date_result["content"]) == 1 assert "type" in date_result["content"][0] assert "text" in date_result["content"][0] @pytest.mark.asyncio async def test_weather_tool_with_coordinates(self): """Test weather tool with coordinate input.""" from src.mcp_server.server import get_weather mock_response = WeatherAPIFixtures.current_weather_response() with patch.object(weather_tool, "make_request", return_value=mock_response): result = await get_weather(location="37.7749,-122.4194") assert "content" in result assert "isError" in result assert result["isError"] is False assert "37.7749,-122.4194" in result["content"][0]["text"] ================================================ FILE: examples/mcp-server-client/tests/fixtures/__init__.py ================================================ ================================================ FILE: examples/mcp-server-client/tests/fixtures/mcp_messages.py ================================================ """Test fixtures for MCP messages and responses.""" from typing import Any class MCPMessageFixtures: """Collection of MCP message fixtures for testing.""" @staticmethod def tool_call_request( tool_name: str, arguments: dict[str, Any], request_id: int = 1 ) -> dict[str, Any]: """Create a standard MCP tool call request.""" return { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": tool_name, "arguments": arguments, }, "id": request_id, } @staticmethod def success_response(content: Any, request_id: int = 1) -> dict[str, Any]: """Create a successful MCP response.""" return { "jsonrpc": "2.0", "id": request_id, "result": { "content": [ { "type": "text", "text": content, } ], "isError": False, }, } @staticmethod def error_response(error_message: str, request_id: int = 1) -> dict[str, Any]: """Create an error MCP response.""" return { "jsonrpc": "2.0", "id": request_id, "result": { "content": [ { "type": "text", "text": error_message, } ], "isError": True, }, } class WeatherAPIFixtures: """Collection of weather API response fixtures.""" @staticmethod def current_weather_response( temperature: float = 20.0, weather_code: int = 0, wind_speed: float = 10.0, humidity: float = 65.0, ) -> dict[str, Any]: """Create a mock Open-Meteo API response.""" return { "current": { "time": "2025-07-07T14:30:00Z", "temperature_2m": temperature, "relative_humidity_2m": humidity, "weather_code": weather_code, "wind_speed_10m": wind_speed, }, "current_units": { "time": "iso8601", "temperature_2m": "°C", "relative_humidity_2m": "%", "weather_code": "wmo code", "wind_speed_10m": "km/h", }, } @staticmethod def api_error_response(status_code: int = 500) -> dict[str, Any]: """Create a mock API error response.""" return { "error": True, "reason": "Internal server error" if status_code == 500 else "Bad request", } ================================================ FILE: examples/mcp-server-client/tests/test_tools/__init__.py ================================================ ================================================ FILE: examples/mcp-server-client/tests/test_tools/test_date_time.py ================================================ """Tests for the date/time tool.""" import zoneinfo from datetime import UTC, datetime from unittest.mock import patch import pytest from src.mcp_server.tools.base import ToolError, ValidationToolError from src.mcp_server.tools.date_time import DateTimeTool class TestDateTimeTool: """Test suite for DateTimeTool.""" @pytest.fixture def datetime_tool(self): """Create a DateTimeTool instance for testing.""" return DateTimeTool() def test_parse_timezone_utc(self, datetime_tool): """Test parsing UTC timezone.""" tz = datetime_tool.parse_timezone("UTC") assert tz == UTC tz = datetime_tool.parse_timezone("utc") assert tz == UTC tz = datetime_tool.parse_timezone("GMT") assert tz == UTC def test_parse_timezone_iana(self, datetime_tool): """Test parsing IANA timezone names.""" tz = datetime_tool.parse_timezone("America/New_York") assert isinstance(tz, zoneinfo.ZoneInfo) assert str(tz) == "America/New_York" tz = datetime_tool.parse_timezone("Europe/London") assert isinstance(tz, zoneinfo.ZoneInfo) assert str(tz) == "Europe/London" def test_parse_timezone_aliases(self, datetime_tool): """Test parsing timezone aliases.""" tz = datetime_tool.parse_timezone("est") assert isinstance(tz, zoneinfo.ZoneInfo) assert str(tz) == "America/New_York" tz = datetime_tool.parse_timezone("pst") assert isinstance(tz, zoneinfo.ZoneInfo) assert str(tz) == "America/Los_Angeles" def test_parse_timezone_invalid(self, datetime_tool): """Test parsing invalid timezone raises ToolError.""" with pytest.raises(ToolError) as exc_info: datetime_tool.parse_timezone("Invalid/Timezone") assert "Invalid timezone" in str(exc_info.value) assert "Invalid/Timezone" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_utc(self, datetime_tool): """Test execution with UTC timezone.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: mock_now = datetime(2025, 7, 7, 14, 30, 25, tzinfo=UTC) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.execute(timezone="UTC") assert result.timezone == "UTC" assert result.datetime == "2025-07-07T14:30:25+00:00" assert isinstance(result.timestamp, float) @pytest.mark.asyncio async def test_execute_iana_timezone(self, datetime_tool): """Test execution with IANA timezone.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: ny_tz = zoneinfo.ZoneInfo("America/New_York") mock_now = datetime(2025, 7, 7, 10, 30, 25, tzinfo=ny_tz) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.execute(timezone="America/New_York") assert result.timezone == "America/New_York" assert "2025-07-07T10:30:25" in result.datetime assert isinstance(result.timestamp, float) @pytest.mark.asyncio async def test_execute_alias(self, datetime_tool): """Test execution with timezone alias.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: ny_tz = zoneinfo.ZoneInfo("America/New_York") mock_now = datetime(2025, 7, 7, 10, 30, 25, tzinfo=ny_tz) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.execute(timezone="est") assert result.timezone == "America/New_York" assert isinstance(result.timestamp, float) @pytest.mark.asyncio async def test_execute_default_timezone(self, datetime_tool): """Test execution with default timezone (UTC).""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: mock_now = datetime(2025, 7, 7, 14, 30, 25, tzinfo=UTC) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.execute() # No timezone argument assert result.timezone == "UTC" assert "2025-07-07T14:30:25" in result.datetime @pytest.mark.asyncio async def test_execute_invalid_timezone(self, datetime_tool): """Test execution with invalid timezone.""" with pytest.raises(ToolError) as exc_info: await datetime_tool.execute(timezone="Invalid/Timezone") assert "Invalid timezone" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_empty_timezone(self, datetime_tool): """Test execution with empty timezone.""" with pytest.raises(ValidationToolError) as exc_info: await datetime_tool.execute(timezone="") assert "Timezone cannot be empty" in str(exc_info.value) def test_format_result(self, datetime_tool): """Test formatting date/time result for display.""" from src.mcp_server.models import DateTimeResponse response = DateTimeResponse( datetime="2025-07-07T14:30:25+00:00", timezone="UTC", timestamp=1720360225.0 ) formatted = datetime_tool.format_result(response) assert "🕐" in formatted assert "2025-07-07" in formatted assert "14:30:25" in formatted assert "UTC" in formatted assert "2025-07-07T14:30:25+00:00" in formatted assert "1720360225" in formatted def test_format_result_weekday(self, datetime_tool): """Test formatting includes weekday information.""" from src.mcp_server.models import DateTimeResponse # Monday response = DateTimeResponse( datetime="2025-07-07T14:30:25+00:00", timezone="UTC", timestamp=1720360225.0 ) formatted = datetime_tool.format_result(response) assert "Monday" in formatted def test_format_result_invalid_datetime(self, datetime_tool): """Test formatting with invalid datetime falls back gracefully.""" from src.mcp_server.models import DateTimeResponse response = DateTimeResponse( datetime="invalid-datetime", timezone="UTC", timestamp=1720360225.0 ) formatted = datetime_tool.format_result(response) # Should still show the basic information assert "🕐" in formatted assert "UTC" in formatted assert "invalid-datetime" in formatted assert "1720360225" in formatted @pytest.mark.asyncio async def test_safe_execute_success(self, datetime_tool): """Test safe_execute returns proper success format.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: mock_now = datetime(2025, 7, 7, 14, 30, 25, tzinfo=UTC) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.safe_execute(timezone="UTC") assert "content" in result assert "isError" in result assert result["isError"] is False assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "🕐" in result["content"][0]["text"] @pytest.mark.asyncio async def test_safe_execute_error(self, datetime_tool): """Test safe_execute returns proper error format.""" result = await datetime_tool.safe_execute(timezone="Invalid/Timezone") assert "content" in result assert "isError" in result assert result["isError"] is True assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "Invalid timezone" in result["content"][0]["text"] def test_get_available_timezones(self, datetime_tool): """Test get_available_timezones returns a list.""" timezones = datetime_tool.get_available_timezones() assert isinstance(timezones, list) assert "UTC" in timezones assert "America/New_York" in timezones assert "Europe/London" in timezones assert len(timezones) > 10 @pytest.mark.asyncio async def test_timezone_case_insensitive(self, datetime_tool): """Test timezone aliases are case insensitive.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: ny_tz = zoneinfo.ZoneInfo("America/New_York") mock_now = datetime(2025, 7, 7, 10, 30, 25, tzinfo=ny_tz) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result1 = await datetime_tool.execute(timezone="EST") result2 = await datetime_tool.execute(timezone="est") assert result1.timezone == result2.timezone == "America/New_York" @pytest.mark.asyncio async def test_whitespace_handling(self, datetime_tool): """Test timezone input with whitespace is handled correctly.""" with patch("src.mcp_server.tools.date_time.datetime") as mock_datetime: mock_now = datetime(2025, 7, 7, 14, 30, 25, tzinfo=UTC) mock_datetime.now.return_value = mock_now mock_datetime.fromisoformat = datetime.fromisoformat result = await datetime_tool.execute(timezone=" UTC ") assert result.timezone == "UTC" ================================================ FILE: examples/mcp-server-client/tests/test_tools/test_dice.py ================================================ """Tests for the dice rolling tool.""" import pytest from src.mcp_server.tools.base import ValidationToolError from src.mcp_server.tools.dice import DiceRollTool class TestDiceRollTool: """Test suite for DiceRollTool.""" @pytest.fixture def dice_tool(self): """Create a DiceRollTool instance for testing.""" return DiceRollTool() @pytest.mark.asyncio async def test_roll_dice_valid_notation(self, dice_tool): """Test valid dice notation works correctly.""" result = await dice_tool.execute(notation="2d6") assert isinstance(result.values, list) assert len(result.values) == 2 assert all(1 <= v <= 6 for v in result.values) assert result.total == sum(result.values) assert result.notation == "2d6" @pytest.mark.asyncio async def test_roll_dice_single_die(self, dice_tool): """Test rolling a single die.""" result = await dice_tool.execute(notation="1d20") assert len(result.values) == 1 assert 1 <= result.values[0] <= 20 assert result.total == result.values[0] assert result.notation == "1d20" @pytest.mark.asyncio async def test_roll_dice_multiple_dice(self, dice_tool): """Test rolling multiple dice.""" result = await dice_tool.execute(notation="4d10") assert len(result.values) == 4 assert all(1 <= v <= 10 for v in result.values) assert result.total == sum(result.values) assert result.notation == "4d10" @pytest.mark.asyncio async def test_roll_dice_invalid_notation_format(self, dice_tool): """Test invalid dice notation raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="d6") assert "Invalid dice notation" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_invalid_notation_no_d(self, dice_tool): """Test notation without 'd' raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="2x6") assert "Invalid dice notation" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_zero_dice_count(self, dice_tool): """Test zero dice count raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="0d6") assert "Dice count must be greater than 0" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_zero_sides(self, dice_tool): """Test zero sides raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="1d0") assert "Number of sides must be greater than 0" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_too_many_dice(self, dice_tool): """Test too many dice raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="101d6") assert "Dice count must not exceed 100" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_too_many_sides(self, dice_tool): """Test too many sides raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="1d1001") assert "Number of sides must not exceed 1000" in str(exc_info.value) @pytest.mark.asyncio async def test_roll_dice_random_text(self, dice_tool): """Test random text raises ValidationError.""" with pytest.raises(ValidationToolError) as exc_info: await dice_tool.execute(notation="abc") assert "Invalid dice notation" in str(exc_info.value) def test_format_result_single_die(self, dice_tool): """Test formatting result for single die.""" from src.mcp_server.models import DiceRollResponse response = DiceRollResponse(values=[15], total=15, notation="1d20") formatted = dice_tool.format_result(response) assert "🎲" in formatted assert "1d20" in formatted assert "**15**" in formatted def test_format_result_multiple_dice(self, dice_tool): """Test formatting result for multiple dice.""" from src.mcp_server.models import DiceRollResponse response = DiceRollResponse(values=[4, 2, 6], total=12, notation="3d6") formatted = dice_tool.format_result(response) assert "🎲" in formatted assert "3d6" in formatted assert "[4, 2, 6]" in formatted assert "**12**" in formatted @pytest.mark.asyncio async def test_safe_execute_success(self, dice_tool): """Test safe_execute returns proper success format.""" result = await dice_tool.safe_execute(notation="2d6") assert "content" in result assert "isError" in result assert result["isError"] is False assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "🎲" in result["content"][0]["text"] @pytest.mark.asyncio async def test_safe_execute_error(self, dice_tool): """Test safe_execute returns proper error format.""" result = await dice_tool.safe_execute(notation="invalid") assert "content" in result assert "isError" in result assert result["isError"] is True assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "Invalid input" in result["content"][0]["text"] @pytest.mark.asyncio async def test_roll_dice_case_insensitive(self, dice_tool): """Test dice notation is case insensitive.""" result1 = await dice_tool.execute(notation="2D6") result2 = await dice_tool.execute(notation="2d6") # Both should work and produce valid results assert len(result1.values) == 2 assert len(result2.values) == 2 assert all(1 <= v <= 6 for v in result1.values) assert all(1 <= v <= 6 for v in result2.values) ================================================ FILE: examples/mcp-server-client/tests/test_tools/test_weather.py ================================================ """Tests for the weather tool.""" from unittest.mock import AsyncMock, patch import httpx import pytest from src.mcp_server.tools.base import ( ExternalServiceError, ToolError, ) from src.mcp_server.tools.weather import WeatherTool from tests.fixtures.mcp_messages import WeatherAPIFixtures class TestWeatherTool: """Test suite for WeatherTool.""" @pytest.fixture def weather_tool(self): """Create a WeatherTool instance for testing.""" return WeatherTool() @pytest.fixture def mock_http_client(self): """Create a mock HTTP client.""" client = AsyncMock() return client def test_parse_location_known_city(self, weather_tool): """Test parsing known city names.""" lat, lon = weather_tool.parse_location("San Francisco") assert lat == 37.7749 assert lon == -122.4194 lat, lon = weather_tool.parse_location("new york") assert lat == 40.7128 assert lon == -74.0060 def test_parse_location_coordinates(self, weather_tool): """Test parsing coordinate strings.""" lat, lon = weather_tool.parse_location("37.7749,-122.4194") assert lat == 37.7749 assert lon == -122.4194 lat, lon = weather_tool.parse_location("51.5074, -0.1278") assert lat == 51.5074 assert lon == -0.1278 def test_parse_location_invalid(self, weather_tool): """Test parsing invalid locations raises ToolError.""" with pytest.raises(ToolError) as exc_info: weather_tool.parse_location("Unknown City") assert "Unknown location" in str(exc_info.value) def test_parse_location_invalid_coordinates(self, weather_tool): """Test parsing invalid coordinates raises ToolError.""" with pytest.raises(ToolError): weather_tool.parse_location("abc,def") with pytest.raises(ToolError): weather_tool.parse_location("200,300") # Out of range def test_weather_code_to_text(self, weather_tool): """Test weather code conversion.""" assert weather_tool.weather_code_to_text(0) == "Clear sky" assert weather_tool.weather_code_to_text(61) == "Slight rain" assert weather_tool.weather_code_to_text(95) == "Thunderstorm" assert "Unknown weather condition" in weather_tool.weather_code_to_text(999) @pytest.mark.asyncio async def test_execute_success_known_city(self, weather_tool, mock_http_client): """Test successful weather retrieval for known city.""" # Mock the API response mock_response = WeatherAPIFixtures.current_weather_response( temperature=18.5, weather_code=2, wind_speed=12.3, humidity=65.0 ) with patch.object(weather_tool, "make_request", return_value=mock_response): result = await weather_tool.execute(location="San Francisco") assert result.location == "San Francisco" assert result.temperature == 18.5 assert result.condition == "Partly cloudy" assert result.wind_speed == 12.3 assert result.humidity == 65.0 @pytest.mark.asyncio async def test_execute_success_coordinates(self, weather_tool): """Test successful weather retrieval using coordinates.""" mock_response = WeatherAPIFixtures.current_weather_response() with patch.object(weather_tool, "make_request", return_value=mock_response): result = await weather_tool.execute(location="37.7749,-122.4194") assert result.location == "37.7749,-122.4194" assert result.temperature == 20.0 assert result.condition == "Clear sky" @pytest.mark.asyncio async def test_execute_invalid_location(self, weather_tool): """Test execution with invalid location.""" with pytest.raises(ToolError) as exc_info: await weather_tool.execute(location="Unknown City") assert "Unknown location" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_empty_location(self, weather_tool): """Test execution with empty location.""" with pytest.raises(ToolError) as exc_info: await weather_tool.execute(location="") assert "Missing required parameter" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_api_timeout(self, weather_tool): """Test handling of API timeout.""" with patch.object( weather_tool, "make_request", side_effect=httpx.TimeoutException("Timeout") ): with pytest.raises(ExternalServiceError) as exc_info: await weather_tool.execute(location="San Francisco") assert "timeout" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_execute_api_http_error(self, weather_tool): """Test handling of HTTP errors.""" error = httpx.HTTPStatusError( "500 Server Error", request=httpx.Request("GET", "http://test"), response=httpx.Response(500, text="Server Error"), ) with patch.object(weather_tool, "make_request", side_effect=error): with pytest.raises(ExternalServiceError) as exc_info: await weather_tool.execute(location="San Francisco") assert "500" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_incomplete_data(self, weather_tool): """Test handling of incomplete API data.""" incomplete_response = {"current": {"time": "2025-07-07T14:30:00Z"}} with patch.object( weather_tool, "make_request", return_value=incomplete_response ): with pytest.raises(ExternalServiceError) as exc_info: await weather_tool.execute(location="San Francisco") assert "Incomplete weather data" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_no_current_data(self, weather_tool): """Test handling of missing current weather data.""" no_current_response = {"forecast": {}} with patch.object( weather_tool, "make_request", return_value=no_current_response ): with pytest.raises(ExternalServiceError) as exc_info: await weather_tool.execute(location="San Francisco") assert "No current weather data" in str(exc_info.value) def test_format_result(self, weather_tool): """Test formatting weather result for display.""" from src.mcp_server.models import WeatherResponse response = WeatherResponse( location="San Francisco", temperature=18.5, condition="Partly cloudy", wind_speed=12.3, humidity=65.0, timestamp="2025-07-07T14:30:00Z", ) formatted = weather_tool.format_result(response) assert "🌤️" in formatted assert "San Francisco" in formatted assert "18.5°C" in formatted assert "Partly cloudy" in formatted assert "12.3 km/h" in formatted assert "65.0%" in formatted def test_format_result_no_humidity(self, weather_tool): """Test formatting weather result without humidity.""" from src.mcp_server.models import WeatherResponse response = WeatherResponse( location="Test City", temperature=20.0, condition="Clear sky", wind_speed=10.0, humidity=None, timestamp=None, ) formatted = weather_tool.format_result(response) assert "🌤️" in formatted assert "Test City" in formatted assert "20.0°C" in formatted assert "💧" not in formatted # Humidity should not appear @pytest.mark.asyncio async def test_safe_execute_success(self, weather_tool): """Test safe_execute returns proper success format.""" mock_response = WeatherAPIFixtures.current_weather_response() with patch.object(weather_tool, "make_request", return_value=mock_response): result = await weather_tool.safe_execute(location="San Francisco") assert "content" in result assert "isError" in result assert result["isError"] is False assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "🌤️" in result["content"][0]["text"] @pytest.mark.asyncio async def test_safe_execute_error(self, weather_tool): """Test safe_execute returns proper error format.""" result = await weather_tool.safe_execute(location="Unknown City") assert "content" in result assert "isError" in result assert result["isError"] is True assert len(result["content"]) == 1 assert result["content"][0]["type"] == "text" assert "Unknown location" in result["content"][0]["text"] @pytest.mark.asyncio async def test_cleanup(self, weather_tool): """Test cleanup method closes HTTP client.""" # Mock the HTTP client mock_client = AsyncMock() weather_tool._http_client = mock_client await weather_tool.cleanup() mock_client.aclose.assert_called_once() assert weather_tool._http_client is None ================================================ FILE: src/__init__.py ================================================ """Defines the application version.""" __version__ = "0.0.2" ================================================ FILE: src/main.py ================================================ """Contains the entrypoint to the app.""" pass ================================================ FILE: src/py.typed ================================================ # PEP 561 – Distributing and Packaging Type Information # https://peps.python.org/pep-0561/ ================================================ FILE: .claude/settings.local.json ================================================ { "permissions": { "allow": [ "Bash(cat:*)", "Bash(find:*)", "Bash(git:diff*)", "Bash(git:log*)", "Bash(git:status*)", "Bash(grep:*)", "Bash(ls:*)", "Bash(mkdir:*)", "Bash(source:*)", "Bash(touch:*)", "Bash(tree:*)", "Bash(uv:run*)", "Edit(AGENTS.md)", "Edit(docs/**/*.md)", "Edit(src/**/*.py)", "Edit(src/**/*.json)", "Edit(tests/**/*.py)", "WebFetch(domain:docs.anthropic.com)" ], "deny": [ "Bash(mv:*)", "Bash(rm:*)", "Edit(CLAUDE.md)" ] } } ================================================ FILE: .claude/commands/execute-prp.md ================================================ # Execute Product Requirements Prompt (PRP) Implement a feature using using the PRP file. ## PRP File: $ARGUMENTS ## Execution Process 1. **Load PRP** - Read the specified PRP file - Understand all context and requirements - Follow all instructions in the PRP and extend the research if needed - Ensure you have all needed context to implement the PRP fully - Do more web searches and codebase exploration as needed 2. **ULTRATHINK** - Think hard before you execute the plan. Create a comprehensive plan addressing all requirements. - Break down complex tasks into smaller, manageable steps using your todos tools. - Use the TodoWrite tool to create and track your implementation plan. - Identify implementation patterns from existing code to follow. 3. **Execute the plan** - Execute the PRP - Implement all the code 4. **Validate** - Run each validation command - Fix any failures - Re-run until all pass 5. **Complete** - Ensure all checklist items done - Run final validation suite - Report completion status - Read the PRP again to ensure you have implemented everything 6. **Reference the PRP** - You can always reference the PRP again if needed Note: If validation fails, use error patterns in PRP to fix and retry. ================================================ FILE: .claude/commands/generate-prp.md ================================================ # Create Product Requirements Prompt (PRP) ## Feature file: $ARGUMENTS Generate a complete PRP (Product Requirements Prompt) for general feature implementation with thorough research. Ensure context is passed to the AI agent to enable self-validation and iterative refinement. Read the feature file first to understand what needs to be created, how the examples provided help, and any other considerations. The AI agent only gets the context you are appending to the PRP and training data. Assume the AI agent has access to the codebase and the same knowledge cutoff as you, so its important that your research findings are included or referenced in the PRP. The Agent has Websearch capabilities, so pass urls to documentation and examples. - Use `/context/PRPs` as `$base_path` - Extract only the filename from `$ARGUMENTS` into `$file_name` ## Research Process 1. **Codebase Analysis** - Search for similar features/patterns in the codebase - Identify files to reference in PRP - Note existing conventions to follow - Check test patterns for validation approach 2. **External Research** - Search for similar features/patterns online - Library documentation (include specific URLs) - Implementation examples (GitHub/StackOverflow/blogs) - Best practices and common pitfalls 3. **User Clarification** (if needed) - Specific patterns to mirror and where to find them? - Integration requirements and where to find them? ## PRP Generation - Use `${base_path}/templates/prp_base.md` in the base folder as template ### Critical Context to Include and pass to the AI agent as part of the PRP - **Documentation**: URLs with specific sections - **Code Examples**: Real snippets from codebase - **Gotchas**: Library quirks, version issues - **Patterns**: Existing approaches to follow ### Implementation Blueprint - Start with pseudocode showing approach - Reference real files for patterns - Include error handling strategy - list tasks to be completed to fullfill the PRP in the order they should be completed ### Validation Gates (Must be Executable) eg for python ```bash # Syntax/Style make ruff make check_types # Unit Tests make coverage_all ``` ***CRITICAL AFTER YOU ARE DONE RESEARCHING AND EXPLORING THE CODEBASE BEFORE YOU START WRITING THE PRP*** ***ULTRATHINK ABOUT THE PRP AND PLAN YOUR APPROACH THEN START WRITING THE PRP*** ## Output - Save the result to `${base_path}/${file_name}` ## Quality Checklist - [ ] All necessary context included - [ ] Validation gates are executable by AI - [ ] References existing patterns - [ ] Clear implementation path - [ ] Error handling documented Score the PRP on a scale of 1-10 (confidence level to succeed in one-pass implementation using claude codes) Remember: The goal is one-pass implementation success through comprehensive context. ================================================ FILE: .devcontainer/setup_python_claude/devcontainer.json ================================================ { "name": "setup_python_claude", "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13", "features": { "ghcr.io/devcontainers/features/node:1": {} }, "customizations": { "vscode": { "extensions": [ "anthropic.claude-code" ] } }, "postCreateCommand": "make setup_python_claude" } ================================================ FILE: .github/dependabot.yaml ================================================ --- # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" ... ================================================ FILE: .github/scripts/create_pr.sh ================================================ #!/bin/bash # 1 base ref, 2 target ref, 3 title suffix # 4 current version, 5 bumped pr_title="PR $2 $3" pr_body="PR automatically created from \`$1\` to bump from \`$4\` to \`$5\` on \`$2\`. Tag \`v$5\` will be created and has to be deleted manually if PR gets closed without merge." gh pr create \ --base $1 \ --head $2 \ --title "${pr_title}" \ --body "${pr_body}" # --label "bump" ================================================ FILE: .github/scripts/delete_branch_pr_tag.sh ================================================ #!/bin/bash # 1 repo, 2 target ref, 3 current version tag_to_delete="v$3" branch_del_api_call="repos/$1/git/refs/heads/$2" del_msg="'$2' force deletion attempted." close_msg="Closing PR '$2' to rollback after failure" echo "Tag $tag_to_delete for $del_msg" git tag -d "$tag_to_delete" echo "PR for $del_msg" gh pr close "$2" --comment "$close_msg" echo "Branch $del_msg" gh api "$branch_del_api_call" -X DELETE && \ echo "Branch without error return deleted." ================================================ FILE: .github/workflows/bump-my-version.yaml ================================================ name: bump-my-version on: # pull_request: # types: [closed] # branches: [main] workflow_dispatch: inputs: bump_type: description: '[major|minor|patch]' required: true default: 'patch' type: choice options: - 'major' - 'minor' - 'patch' env: BRANCH_NEW: "bump-${{ github.run_number }}-${{ github.ref_name }}" SKIP_PR_HINT: "[skip ci bump]" SCRIPT_PATH: ".github/scripts" jobs: bump_my_version: # TODO bug? currently resulting in: Unrecognized named-value: 'env'. # https://stackoverflow.com/questions/61238849/github-actions-if-contains-function-not-working-with-env-variable/61240761 # if: !contains( # github.event.pull_request.title, # ${{ env.SKIP_PR_HINT }} # ) # TODO check for PR closed by bot to avoid PR creation loop # github.actor != 'github-actions' if: > github.event_name == 'workflow_dispatch' || ( github.event.pull_request.merged == true && github.event.pull_request.closed_by != 'github-actions' ) runs-on: ubuntu-latest outputs: branch_new: ${{ steps.create_branch.outputs.branch_new }} summary_data: ${{ steps.set_summary.outputs.summary_data }} permissions: actions: read checks: write contents: write pull-requests: write steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set git cfg and create branch id: create_branch run: | git config user.email "github-actions@users.noreply.github.com" git config user.name "github-actions[bot]" git checkout -b "${{ env.BRANCH_NEW }}" echo "branch_new=${{ env.BRANCH_NEW }}" >> $GITHUB_OUTPUT - name: Bump version id: bump uses: callowayproject/bump-my-version@0.29.0 env: BUMPVERSION_TAG: "true" with: args: ${{ inputs.bump_type }} branch: ${{ env.BRANCH_NEW }} - name: "Create PR '${{ env.BRANCH_NEW }}'" if: steps.bump.outputs.bumped == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | src="${{ env.SCRIPT_PATH }}/create_pr.sh" chmod +x "$src" $src "${{ github.ref_name }}" "${{ env.BRANCH_NEW }}" "${{ env.SKIP_PR_HINT }}" "${{ steps.bump.outputs.previous-version }}" "${{ steps.bump.outputs.current-version }}" - name: Delete branch, PR and tag in case of failure or cancel if: failure() || cancelled() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | src="${{ env.SCRIPT_PATH }}/delete_branch_pr_tag.sh" chmod +x "$src" $src "${{ github.repository }}" "${{ env.BRANCH_NEW }}" "${{ steps.bump.outputs.current-version }}" - name: Set summary data id: set_summary if: ${{ always() }} run: echo "summary_data=${GITHUB_STEP_SUMMARY}" >> $GITHUB_OUTPUT generate_summary: name: Generate Summary Report if: ${{ always() }} needs: bump_my_version uses: ./.github/workflows/summarize-jobs-reusable.yaml with: branch_to_summarize: ${{ needs.bump_my_version.outputs.branch_new }} summary_data: ${{ needs.bump_my_version.outputs.summary_data }} ================================================ FILE: .github/workflows/codeql.yaml ================================================ --- # https://github.blog/changelog/2023-01-18-code-scanning-codeql-action-v1-is-now-deprecated/ name: "CodeQL" on: push: pull_request: types: [closed] branches: [ main ] schedule: - cron: '27 11 * * 0' workflow_dispatch: jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@v3 # if autobuild fails #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 #- name: sarif # uses: github/codeql-action/upload-sarif@v2 ... ================================================ FILE: .github/workflows/generate-deploy-mkdocs-ghpages.yaml ================================================ --- name: Deploy Docs on: pull_request: types: [closed] branches: [main] workflow_dispatch: env: DOCSTRINGS_FILE: "docstrings.md" DOC_DIR: "docs" SRC_DIR: "src" SITE_DIR: "site" IMG_DIR: "assets/images" jobs: build-and-deploy: runs-on: ubuntu-latest permissions: contents: read pages: write id-token: write environment: name: github-pages steps: - name: Checkout the repository uses: actions/checkout@v4.0.0 with: ref: ${{ github.event.pull_request.merged == true && 'main' || github.ref_name }} fetch-depth: 0 - uses: actions/configure-pages@v5.0.0 # caching instead of actions/cache@v4.0.0 # https://docs.astral.sh/uv/guides/integration/github/#caching - name: Install uv with cache dependency glob uses: astral-sh/setup-uv@v5.0.0 with: enable-cache: true cache-dependency-glob: "uv.lock" # setup python from pyproject.toml using uv # instead of using actions/setup-python@v5.0.0 # https://docs.astral.sh/uv/guides/integration/github/#setting-up-python - name: "Set up Python" run: uv python install - name: Install only doc deps run: uv sync --only-group docs # --frozen - name: Get repo info and stream into mkdocs.yaml id: repo_info run: | REPO_INFO=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/${{ github.repository }}) REPO_URL="${{ github.server_url }}/${{ github.repository }}" REPO_URL=$(echo ${REPO_URL} | sed 's|/|\\/|g') SITE_NAME=$(sed '1!d' README.md | sed '0,/# /{s/# //}') SITE_DESC=$(echo $REPO_INFO | jq -r .description) sed -i "s//${REPO_URL}/g" mkdocs.yaml sed -i "s//${SITE_NAME}/g" mkdocs.yaml sed -i "s//${SITE_DESC}/g" mkdocs.yaml - name: Copy text files to be included run: | CFG_PATH="src/app/config" mkdir -p "${DOC_DIR}/${CFG_PATH}" cp README.md "${DOC_DIR}/index.md" cp CHANGELOG.md LICENSE "${DOC_DIR}" # Auxiliary files cp .env.example "${DOC_DIR}" - name: Generate code docstrings concat file run: | PREFIX="::: " find "${SRC_DIR}" -type f -name "*.py" \ -type f -not -name "__*__*" -printf "%P\n" | \ sed 's/\//./g' | sed 's/\.py$//' | \ sed "s/^/${PREFIX}/" | sort > \ "${DOC_DIR}/${DOCSTRINGS_FILE}" - name: Build documentation run: uv run --locked --only-group docs mkdocs build - name: Copy image files to be included run: | # copy images, mkdocs does not by default # mkdocs also overwrites pre-made directories dir="${{ env.SITE_DIR }}/${{ env.IMG_DIR }}" if [ -d "${{ env.IMG_DIR }}" ]; then mkdir -p "${dir}" cp "${{ env.IMG_DIR }}"/* "${dir}" fi # - name: Push to gh-pages # run: uv run mkdocs gh-deploy --force - name: Upload artifact uses: actions/upload-pages-artifact@v3.0.0 with: path: "${{ env.SITE_DIR }}" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4.0.0 ... ================================================ FILE: .github/workflows/links-fail-fast.yaml ================================================ --- # https://github.com/lycheeverse/lychee-action # https://github.com/marketplace/actions/lychee-broken-link-checker name: "Link Checker" on: workflow_dispatch: push: branches-ignore: [main] pull_request: types: [closed] branches: [main] schedule: - cron: "00 00 * * 0" jobs: linkChecker: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@v4 - name: Link Checker id: lychee uses: lycheeverse/lychee-action@v2 - name: Create Issue From File if: steps.lychee.outputs.exit_code != 0 uses: peter-evans/create-issue-from-file@v5 with: title: lychee Link Checker Report content-filepath: ./lychee/out.md labels: report, automated issue ... ================================================ FILE: .github/workflows/pytest.yaml ================================================ name: pytest on: workflow_dispatch: jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest - name: Run tests run: pytest ================================================ FILE: .github/workflows/ruff.yaml ================================================ --- # https://github.com/astral-sh/ruff-action # https://github.com/astral-sh/ruff name: ruff on: push: pull_request: types: [closed] branches: [main] schedule: - cron: "0 0 * * 0" workflow_dispatch: jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/ruff-action@v3 ... ================================================ FILE: .github/workflows/summarize-jobs-reusable.yaml ================================================ --- # https://ecanarys.com/supercharging-github-actions-with-job-summaries-and-pull-request-comments/ # FIXME currently bug in gha summaries ? $GITHUB_STEP_SUMMARY files are empty # https://github.com/orgs/community/discussions/110283 # https://github.com/orgs/community/discussions/67991 # Possible workaround # echo ${{ fromJSON(step).name }}" >> $GITHUB_STEP_SUMMARY # echo ${{ fromJSON(step).outcome }}" >> $GITHUB_STEP_SUMMARY # echo ${{ fromJSON(step).conclusion }}" name: Summarize workflow jobs on: workflow_call: outputs: summary: description: "Outputs summaries of jobs in a workflow" value: ${{ jobs.generate_summary.outputs.summary }} inputs: branch_to_summarize: required: false default: 'main' type: string summary_data: required: false type: string jobs: generate_summary: name: Generate Summary runs-on: ubuntu-latest permissions: contents: read actions: read checks: read pull-requests: none outputs: summary: ${{ steps.add_changed_files.outputs.summary }} steps: - name: Add general information id: general_info run: | echo "# Job Summaries" >> $GITHUB_STEP_SUMMARY echo "Job: `${{ github.job }}`" >> $GITHUB_STEP_SUMMARY echo "Date: $(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_STEP_SUMMARY - name: Add step states id: step_states run: | echo "### Steps:" >> $GITHUB_STEP_SUMMARY # loop summary_data if valid json if jq -e . >/dev/null 2>&1 <<< "${{ inputs.summary_data }}"; then jq -r ' .steps[] | select(.conclusion != null) | "- **\(.name)**: \( if .conclusion == "success" then ":white_check_mark:" elif .conclusion == "failure" then ":x:" else ":warning:" end )" ' <<< "${{ inputs.summary_data }}" >> $GITHUB_STEP_SUMMARY else echo "Invalid JSON in summary data." >> $GITHUB_STEP_SUMMARY fi - name: Checkout repo uses: actions/checkout@v4 with: ref: "${{ inputs.branch_to_summarize }}" fetch-depth: 0 - name: Add changed files since last push id: add_changed_files run: | # Get the tags # Use disabled lines to get last two commits # current=$(git show -s --format=%ci HEAD) # previous=$(git show -s --format=%ci HEAD~1) # git diff --name-only HEAD^ HEAD >> $GITHUB_STEP_SUMMARY version_tag_regex="^v[0-9]+\.[0-9]+\.[0-9]+$" # v0.0.0 tags=$(git tag --sort=-version:refname | \ grep -E "${version_tag_regex}" || echo "") # Get latest and previous tags latest_tag=$(echo "${tags}" | head -n 1) previous_tag=$(echo "${tags}" | head -n 2 | tail -n 1) echo "tags: latest '${latest_tag}', previous '${previous_tag}'" # Write to summary error_msg="No files to output. Tag not found:" echo ${{ steps.step_states.outputs.summary }} >> $GITHUB_STEP_SUMMARY echo "## Changed files on '${{ inputs.branch_to_summarize }}'" >> $GITHUB_STEP_SUMMARY if [ -z "${latest_tag}" ]; then echo "${error_msg} latest" >> $GITHUB_STEP_SUMMARY elif [ -z "${previous_tag}" ]; then echo "${error_msg} previous" >> $GITHUB_STEP_SUMMARY elif [ "${latest_tag}" == "${previous_tag}" ]; then echo "Latest and previous tags are the same: '${latest_tag}'" >> $GITHUB_STEP_SUMMARY else # Get commit dates and hashes latest_date=$(git log -1 --format=%ci $latest_tag) previous_date=$(git log -1 --format=%ci $previous_tag) current_hash=$(git rev-parse --short $latest_tag) previous_hash=$(git rev-parse --short $previous_tag) # Append summary to the job summary echo "Latest Tag Commit: '${latest_tag}' (${current_hash}) ${latest_date}" >> $GITHUB_STEP_SUMMARY echo "Previous Tag Commit: '${previous_tag}' (${previous_hash}) ${previous_date}" >> $GITHUB_STEP_SUMMARY echo "Files changed:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY git diff --name-only $previous_tag..$latest_tag >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY fi - name: Output error message in case of failure or cancel if: failure() || cancelled() run: | if [ "${{ job.status }}" == "cancelled" ]; then out_msg="## Workflow was cancelled" else out_msg="## Error in previous step" fi echo $out_msg >> $GITHUB_STEP_SUMMARY ... ================================================ FILE: .github/workflows/write-llms-txt.yaml ================================================ # TODO use local installation of repo to text # https://github.com/itsitgroup/repo2txt name: Write repo llms.txt on: push: branches: [main] workflow_dispatch: inputs: LLMS_TXT_PATH: description: 'Path to the directory to save llsm.txt' required: true default: 'docs' type: string LLMS_TXT_NAME: description: 'Path to the directory to save llsm.txt' required: true default: 'llms.txt' type: string CONVERTER_URL: description: '[uithub|gittodoc]' # |repo2txt required: true default: 'uithub.com' type: choice options: - 'uithub.com' - 'gittodoc.com' # - 'repo2txt.com' jobs: generate-file: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Construct and create llms.txt path id: construct_and_create_llms_txt_path run: | LLMS_TXT_PATH="${{ inputs.LLMS_TXT_PATH }}" LLMS_TXT_PATH="${LLMS_TXT_PATH:-docs}" LLMS_TXT_NAME="${{ inputs.LLMS_TXT_NAME }}" LLMS_TXT_NAME="${LLMS_TXT_NAME:-llms.txt}" echo "LLMS_TXT_FULL=${LLMS_TXT_PATH}/${LLMS_TXT_NAME}" >> $GITHUB_OUTPUT mkdir -p "${LLMS_TXT_PATH}" - name: Fetch TXT from URL run: | LLMS_TXT_FULL=${{ steps.construct_and_create_llms_txt_path.outputs.LLMS_TXT_FULL }} URL="https://${{ inputs.CONVERTER_URL }}/${{ github.repository }}" echo "Fetching content from: ${URL}" echo "Saving content to: ${LLMS_TXT_FULL}" curl -s "${URL}" > "${LLMS_TXT_FULL}" - name: Commit and push file run: | LLMS_TXT_FULL=${{ steps.construct_and_create_llms_txt_path.outputs.LLMS_TXT_FULL }} commit_msg="feat(docs): Add/Update ${LLMS_TXT_FULL}, a flattened repo as single text file, inspired by [llmstxt.org](https://llmstxt.org/)." git config user.name "github-actions" git config user.email "github-actions@github.com" git add "${LLMS_TXT_FULL}" git commit -m "${commit_msg}" git push