├── .python-version ├── renovate.json ├── tests ├── fixtures │ └── example.py ├── test_integration.py └── test_unit.py ├── .gitignore ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ └── test.yml ├── README.md ├── ast-grep.mdc ├── main.py └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/example.py: -------------------------------------------------------------------------------- 1 | def hello(): 2 | print("Hello, World!") 3 | 4 | 5 | def add(a, b): 6 | return a + b 7 | 8 | 9 | class Calculator: 10 | def multiply(self, x, y): 11 | return x * y 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # MyPy. Ruff, PyTest cache folders 10 | .*_cache/ 11 | 12 | # Virtual environments 13 | .venv 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Herrington Darkholme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sg-mcp" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "pydantic>=2.11.0", 9 | "mcp[cli]>=1.6.0", 10 | "pyyaml>=6.0.2", 11 | ] 12 | 13 | [project.optional-dependencies] 14 | dev = [ 15 | "pytest>=8.0.0", 16 | "pytest-cov>=5.0.0", 17 | "pytest-mock>=3.14.0", 18 | "ruff>=0.7.0", 19 | "mypy>=1.13.0", 20 | "types-pyyaml>=6.0.12.20250809", 21 | ] 22 | 23 | [project.scripts] 24 | ast-grep-server = "main:run_mcp_server" 25 | 26 | [tool.pytest.ini_options] 27 | testpaths = ["tests"] 28 | python_files = ["test_*.py"] 29 | python_classes = ["Test*"] 30 | python_functions = ["test_*"] 31 | addopts = "-v" 32 | 33 | [tool.coverage.run] 34 | source = ["main"] 35 | omit = ["tests/*"] 36 | 37 | [tool.coverage.report] 38 | exclude_lines = [ 39 | "pragma: no cover", 40 | "def __repr__", 41 | "if __name__ == .__main__.:", 42 | "raise NotImplementedError", 43 | "pass", 44 | "except ImportError:", 45 | ] 46 | 47 | [tool.ruff] 48 | line-length = 140 49 | target-version = "py313" 50 | 51 | [tool.ruff.lint] 52 | select = ["E", "F", "I", "N", "W"] 53 | 54 | [tool.mypy] 55 | python_version = "3.13" 56 | warn_return_any = true 57 | warn_unused_configs = true 58 | disallow_untyped_defs = false 59 | ignore_missing_imports = true 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 2 | name: Tests 3 | 4 | on: # https://docs.github.com/en/actions/reference/events-that-trigger-workflows 5 | push: 6 | branches: 7 | - '**' 8 | tags-ignore: # don't build tags 9 | - '**' 10 | paths-ignore: 11 | - '**/*.md' 12 | pull_request: 13 | paths-ignore: 14 | - '**/*.md' 15 | workflow_dispatch: 16 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch 17 | 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | jobs: 23 | test: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest, windows-latest, macos-latest] 28 | fail-fast: false 29 | 30 | steps: 31 | - name: Git Checkout 32 | uses: actions/checkout@v6 # https://github.com/actions/checkout 33 | 34 | - name: Install uv 35 | uses: astral-sh/setup-uv@v7 36 | with: 37 | enable-cache: true 38 | 39 | - name: Set up Python 40 | run: uv python install 41 | 42 | - name: Install ast-grep 43 | run: | 44 | npm install -g @ast-grep/cli 45 | ast-grep --version 46 | 47 | - name: Install dependencies 48 | run: | 49 | uv sync --all-extras --dev 50 | 51 | - name: Lint with ruff 52 | run: | 53 | uv run ruff check . 54 | 55 | - name: Format check with ruff 56 | run: | 57 | uv run ruff format --check . 58 | continue-on-error: true # TODO 59 | 60 | - name: Type check with mypy 61 | run: | 62 | uv run mypy main.py 63 | 64 | - name: Run unit tests 65 | run: | 66 | uv run pytest tests/test_unit.py -v --cov=main --cov-report=term-missing 67 | 68 | - name: Run integration tests 69 | run: | 70 | uv run pytest tests/test_integration.py -v 71 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | """Integration tests for ast-grep MCP server""" 2 | 3 | import json 4 | import os 5 | import sys 6 | from unittest.mock import Mock, patch 7 | 8 | import pytest 9 | 10 | # Add parent directory to path 11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | 13 | 14 | # Mock FastMCP to disable decoration 15 | class MockFastMCP: 16 | """Mock FastMCP that returns functions unchanged""" 17 | 18 | def __init__(self, name): 19 | self.name = name 20 | self.tools = {} # Store registered tools 21 | 22 | def tool(self, **kwargs): 23 | """Decorator that returns the function unchanged""" 24 | 25 | def decorator(func): 26 | # Store the function for later retrieval 27 | self.tools[func.__name__] = func 28 | return func # Return original function without modification 29 | 30 | return decorator 31 | 32 | def run(self, **kwargs): 33 | """Mock run method""" 34 | pass 35 | 36 | 37 | # Mock the Field function to return the default value 38 | def mock_field(**kwargs): 39 | return kwargs.get("default") 40 | 41 | 42 | # Import with mocked decorators 43 | with patch("mcp.server.fastmcp.FastMCP", MockFastMCP): 44 | with patch("pydantic.Field", mock_field): 45 | import main 46 | 47 | # Call register_mcp_tools to define the tool functions 48 | main.register_mcp_tools() 49 | 50 | # Extract the tool functions from the mocked mcp instance 51 | find_code = main.mcp.tools.get("find_code") 52 | find_code_by_rule = main.mcp.tools.get("find_code_by_rule") 53 | 54 | 55 | @pytest.fixture 56 | def fixtures_dir(): 57 | """Get the path to the fixtures directory""" 58 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "fixtures")) 59 | 60 | 61 | class TestIntegration: 62 | """Integration tests for ast-grep MCP functions""" 63 | 64 | def test_find_code_text_format(self, fixtures_dir): 65 | """Test find_code with text format""" 66 | result = find_code( 67 | project_folder=fixtures_dir, 68 | pattern="def $NAME($$$)", 69 | language="python", 70 | output_format="text", 71 | ) 72 | 73 | assert "hello" in result 74 | assert "add" in result 75 | assert "Found" in result and "matches" in result 76 | 77 | def test_find_code_json_format(self, fixtures_dir): 78 | """Test find_code with JSON format""" 79 | result = find_code( 80 | project_folder=fixtures_dir, 81 | pattern="def $NAME($$$)", 82 | language="python", 83 | output_format="json", 84 | ) 85 | 86 | assert len(result) >= 2 87 | assert any("hello" in str(match) for match in result) 88 | assert any("add" in str(match) for match in result) 89 | 90 | @patch("main.run_ast_grep") 91 | def test_find_code_by_rule(self, mock_run, fixtures_dir): 92 | """Test find_code_by_rule with mocked ast-grep""" 93 | # Mock the response with JSON format (since we always use JSON internally) 94 | mock_result = Mock() 95 | mock_matches = [{ 96 | "text": "class Calculator:\n pass", 97 | "file": "fixtures/example.py", 98 | "range": {"start": {"line": 6}, "end": {"line": 7}} 99 | }] 100 | mock_result.stdout = json.dumps(mock_matches) 101 | mock_run.return_value = mock_result 102 | 103 | yaml_rule = """id: test 104 | language: python 105 | rule: 106 | pattern: class $NAME""" 107 | 108 | result = find_code_by_rule( 109 | project_folder=fixtures_dir, yaml=yaml_rule, output_format="text" 110 | ) 111 | 112 | assert "Calculator" in result 113 | assert "Found 1 match" in result 114 | assert "fixtures/example.py:7-8" in result 115 | 116 | # Verify the command was called correctly 117 | mock_run.assert_called_once_with( 118 | "scan", ["--inline-rules", yaml_rule, "--json", fixtures_dir] 119 | ) 120 | 121 | def test_find_code_with_max_results(self, fixtures_dir): 122 | """Test find_code with max_results parameter""" 123 | result = find_code( 124 | project_folder=fixtures_dir, 125 | pattern="def $NAME($$$)", 126 | language="python", 127 | max_results=1, 128 | output_format="text", 129 | ) 130 | 131 | # The new format says "showing first X of Y" instead of "limited to X" 132 | assert "showing first 1 of" in result or "Found 1 match" in result 133 | # Should only have one match in the output 134 | assert result.count("def ") == 1 135 | 136 | def test_find_code_no_matches(self, fixtures_dir): 137 | """Test find_code when no matches are found""" 138 | result = find_code( 139 | project_folder=fixtures_dir, 140 | pattern="nonexistent_pattern_xyz", 141 | output_format="text", 142 | ) 143 | 144 | assert result == "No matches found" 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ast-grep MCP Server 2 | 3 | An experimental [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides AI assistants with powerful structural code search capabilities using [ast-grep](https://ast-grep.github.io/). 4 | 5 | ## Overview 6 | 7 | This MCP server enables AI assistants (like Cursor, Claude Desktop, etc.) to search and analyze codebases using Abstract Syntax Tree (AST) pattern matching rather than simple text-based search. By leveraging ast-grep's structural search capabilities, AI can: 8 | 9 | - Find code patterns based on syntax structure, not just text matching 10 | - Search for specific programming constructs (functions, classes, imports, etc.) 11 | - Write and test complex search rules using YAML configuration 12 | - Debug and visualize AST structures for better pattern development 13 | 14 | ## Prerequisites 15 | 16 | 1. **Install ast-grep**: Follow [ast-grep installation guide](https://ast-grep.github.io/guide/quick-start.html#installation) 17 | ```bash 18 | # macOS 19 | brew install ast-grep 20 | nix-shell -p ast-grep 21 | cargo install ast-grep --locked 22 | ``` 23 | 24 | 2. **Install uv**: Python package manager 25 | ```bash 26 | curl -LsSf https://astral.sh/uv/install.sh | sh 27 | ``` 28 | 29 | 3. **MCP-compatible client**: Such as Cursor, Claude Desktop, or other MCP clients 30 | 31 | ## Installation 32 | 33 | 1. Clone this repository: 34 | ```bash 35 | git clone https://github.com/ast-grep/ast-grep-mcp.git 36 | cd ast-grep-mcp 37 | ``` 38 | 39 | 2. Install dependencies: 40 | ```bash 41 | uv sync 42 | ``` 43 | 44 | 3. Verify ast-grep installation: 45 | ```bash 46 | ast-grep --version 47 | ``` 48 | 49 | ## Running with `uvx` 50 | 51 | You can run the server directly from GitHub using `uvx`: 52 | 53 | ```bash 54 | uvx --from git+https://github.com/ast-grep/ast-grep-mcp ast-grep-server 55 | ``` 56 | 57 | This is useful for quickly trying out the server without cloning the repository. 58 | 59 | ## Configuration 60 | 61 | ### For Cursor 62 | 63 | Add to your MCP settings (usually in `.cursor-mcp/settings.json`): 64 | 65 | ```json 66 | { 67 | "mcpServers": { 68 | "ast-grep": { 69 | "command": "uv", 70 | "args": ["--directory", "/absolute/path/to/ast-grep-mcp", "run", "main.py"], 71 | "env": {} 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ### For Claude Desktop 78 | 79 | Add to your Claude Desktop MCP configuration: 80 | 81 | ```json 82 | { 83 | "mcpServers": { 84 | "ast-grep": { 85 | "command": "uv", 86 | "args": ["--directory", "/absolute/path/to/ast-grep-mcp", "run", "main.py"], 87 | "env": {} 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | ### Custom ast-grep Configuration 94 | 95 | The MCP server supports using a custom `sgconfig.yaml` file to configure ast-grep behavior. 96 | See the [ast-grep configuration documentation](https://ast-grep.github.io/guide/project/project-config.html) for details on the config file format. 97 | 98 | You can provide the config file in two ways (in order of precedence): 99 | 100 | 1. **Command-line argument**: `--config /path/to/sgconfig.yaml` 101 | 2. **Environment variable**: `AST_GREP_CONFIG=/path/to/sgconfig.yaml` 102 | 103 | ## Usage 104 | 105 | This repository includes comprehensive ast-grep rule documentation in [ast-grep.mdc](https://github.com/ast-grep/ast-grep-mcp/blob/main/ast-grep.mdc). The documentation covers all aspects of writing effective ast-grep rules, from simple patterns to complex multi-condition searches. 106 | 107 | You can add it to your cursor rule or Claude.md, and attach it when you need AI agent to create ast-grep rule for you. 108 | 109 | The prompt will ask LLM to use MCP to create, verify and improve the rule it creates. 110 | 111 | ## Features 112 | 113 | The server provides four main tools for code analysis: 114 | 115 | ### 🔍 `dump_syntax_tree` 116 | Visualize the Abstract Syntax Tree structure of code snippets. Essential for understanding how to write effective search patterns. 117 | 118 | **Use cases:** 119 | - Debug why a pattern isn't matching 120 | - Understand the AST structure of target code 121 | - Learn ast-grep pattern syntax 122 | 123 | ### 🧪 `test_match_code_rule` 124 | Test ast-grep YAML rules against code snippets before applying them to larger codebases. 125 | 126 | **Use cases:** 127 | - Validate rules work as expected 128 | - Iterate on rule development 129 | - Debug complex matching logic 130 | 131 | ### 🎯 `find_code` 132 | Search codebases using simple ast-grep patterns for straightforward structural matches. 133 | 134 | **Parameters:** 135 | - `max_results`: Limit number of complete matches returned (default: unlimited) 136 | - `output_format`: Choose between `"text"` (default, ~75% fewer tokens) or `"json"` (full metadata) 137 | 138 | **Text Output Format:** 139 | ``` 140 | Found 2 matches: 141 | 142 | path/to/file.py:10-15 143 | def example_function(): 144 | # function body 145 | return result 146 | 147 | path/to/file.py:20-22 148 | def another_function(): 149 | pass 150 | ``` 151 | 152 | **Use cases:** 153 | - Find function calls with specific patterns 154 | - Locate variable declarations 155 | - Search for simple code constructs 156 | 157 | ### 🚀 `find_code_by_rule` 158 | Advanced codebase search using complex YAML rules that can express sophisticated matching criteria. 159 | 160 | **Parameters:** 161 | - `max_results`: Limit number of complete matches returned (default: unlimited) 162 | - `output_format`: Choose between `"text"` (default, ~75% fewer tokens) or `"json"` (full metadata) 163 | 164 | **Use cases:** 165 | - Find nested code structures 166 | - Search with relational constraints (inside, has, precedes, follows) 167 | - Complex multi-condition searches 168 | 169 | 170 | ## Usage Examples 171 | 172 | ### Basic Pattern Search 173 | 174 | Use Query: 175 | 176 | > Find all console.log statements 177 | 178 | AI will generate rules like: 179 | 180 | ```yaml 181 | id: find-console-logs 182 | language: javascript 183 | rule: 184 | pattern: console.log($$$) 185 | ``` 186 | 187 | ### Complex Rule Example 188 | 189 | User Query: 190 | > Find async functions that use await 191 | 192 | AI will generate rules like: 193 | 194 | ```yaml 195 | id: async-with-await 196 | language: javascript 197 | rule: 198 | all: 199 | - kind: function_declaration 200 | - has: 201 | pattern: async 202 | - has: 203 | pattern: await $EXPR 204 | stopBy: end 205 | ``` 206 | 207 | ## Supported Languages 208 | 209 | ast-grep supports many programming languages including: 210 | - JavaScript/TypeScript 211 | - Python 212 | - Rust 213 | - Go 214 | - Java 215 | - C/C++ 216 | - C# 217 | - And many more... 218 | 219 | For a complete list of built-in supported languages, see the [ast-grep language support documentation](https://ast-grep.github.io/reference/languages.html). 220 | 221 | You can also add support for custom languages through the `sgconfig.yaml` configuration file. See the [custom language guide](https://ast-grep.github.io/guide/project/project-config.html#languagecustomlanguage) for details. 222 | 223 | ## Troubleshooting 224 | 225 | ### Common Issues 226 | 227 | 1. **"Command not found" errors**: Ensure ast-grep is installed and in your PATH 228 | 2. **No matches found**: Try adding `stopBy: end` to relational rules 229 | 3. **Pattern not matching**: Use `dump_syntax_tree` to understand the AST structure 230 | 4. **Permission errors**: Ensure the server has read access to target directories 231 | 232 | ## Contributing 233 | 234 | This is an experimental project. Issues and pull requests are welcome! 235 | 236 | ## Related Projects 237 | 238 | - [ast-grep](https://ast-grep.github.io/) - The core structural search tool 239 | - [Model Context Protocol](https://modelcontextprotocol.io/) - The protocol this server implements 240 | - [FastMCP](https://github.com/pydantic/fastmcp) - The Python MCP framework used 241 | - [Codemod MCP](https://docs.codemod.com/model-context-protocol) - Gives AI assistants tools like tree-sitter AST and node types, ast-grep instructions (YAML and JS ast-grep), and Codemod CLI commands to easily build, publish, and run ast-grep based codemods. 242 | 243 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/ast-grep-ast-grep-mcp-badge.png)](https://mseep.ai/app/ast-grep-ast-grep-mcp) 244 | -------------------------------------------------------------------------------- /ast-grep.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | 7 | # use ast-grep to search code 8 | 9 | Your task is to help users to write ast-grep rules to search code. 10 | User will query you by natural language, and you will write ast-grep rules to search code. 11 | 12 | You need to translate user's query into ast-grep rules. 13 | And use ast-grep-mcp to develop a rule, test the rule and then search the codebase. 14 | 15 | ## General Process 16 | 17 | 1. Clearly understand the user's query. Clarify any ambiguities and if needed, ask user for more details. 18 | 2. Write a simple example code snippet that matches the user's query. 19 | 3. Write an ast-grep rule that matches the example code snippet. 20 | 4. Test the rule against the example code snippet to ensure it matches. Use ast-grep mcp tool `test_match_code_rule` to verify the rule. 21 | a. if the rule does not match, revise the rule by removing some sub rules and debugging unmatching parts. 22 | b. if you are using `inside` or `has` relational rules, ensure to use `stopBy: end` to ensure the search goes to the end of the direction. 23 | 5. Use the ast-grep mcp tool to search code using the rule. 24 | 25 | ## Tips for Writing Rules 26 | 27 | 0. always use `stopBy: end` for relational rules to ensure the search goes to the end of the direction. 28 | 29 | ```yaml 30 | has: 31 | pattern: await $EXPR 32 | stopBy: end 33 | ``` 34 | 35 | 1. if relational rules are used but no match is found, try adding `stopBy: end` to the relational rule to ensure it searches to the end of the direction. 36 | 2. use pattern only if the code structure is simple and does not require complex matching (e.g. matching function calls, variable names, etc.). 37 | 3. use rule if the code structure is complex and can be broken down into smaller parts (e.g. find call inside certain function). 38 | 4. if pattern is not working, try using `kind` to match the node type first, then use `has` or `inside` to match the code structure. 39 | 40 | ## Rule Development Process 41 | 1. Break down the user's query into smaller parts. 42 | 2. Identify sub rules that can be used to match the code. 43 | 3. Combine the sub rules into a single rule using relational rules or composite rules. 44 | 4. if rule does not match example code, revise the rule by removing some sub rules and debugging unmatching parts. 45 | 5. Use ast-grep mcp tool to dump AST or dump pattern query 46 | 6. Use ast-grep mcp tool to test the rule against the example code snippet. 47 | 48 | ## ast-grep mcp tool usage 49 | 50 | ast-grep mcp has several tools: 51 | - dump_syntax_tree will dump the AST of the code, this is useful for debugging and understanding the code structure and patterns 52 | - test_match_code_rule will test a rule agains a code snippet, this is useful to ensure the rule matches the code 53 | 54 | ## Rule Format 55 | 56 | # ast-grep Rule Documentation for Claude Code 57 | 58 | ## 1. Introduction to ast-grep Rules 59 | 60 | ast-grep rules are declarative specifications for matching and filtering Abstract Syntax Tree (AST) nodes. They enable structural code search and analysis by defining conditions an AST node must meet to be matched. 61 | 62 | ### 1.1 Overview of Rule Categories 63 | 64 | ast-grep rules are categorized into three types for modularity and comprehensive definition : 65 | * **Atomic Rules**: Match individual AST nodes based on intrinsic properties like code patterns (`pattern`), node type (`kind`), or text content (`regex`). 66 | * **Relational Rules**: Define conditions based on a target node's position or relationship to other nodes (e.g., `inside`, `has`, `precedes`, `follows`). 67 | * **Composite Rules**: Combine other rules using logical operations (AND, OR, NOT) to form complex matching criteria (e.g., `all`, `any`, `not`, `matches`). 68 | 69 | ## 2. Anatomy of an ast-grep Rule Object 70 | 71 | The ast-grep rule object is the core configuration unit defining how ast-grep identifies and filters AST nodes. It's typically a YAML. 72 | 73 | ### 2.1 General Structure and Optionality 74 | 75 | Every field within an ast-grep Rule Object is optional, but at least one "positive" key (e.g., `kind`, `pattern`) must be present. 76 | 77 | A node matches a rule if it satisfies all fields defined within that rule object, implying an implicit logical AND operation. 78 | 79 | For rules using metavariables that depend on prior matching, explicit `all` composite rules are recommended to guarantee execution order. 80 | 81 | **Table 1: ast-grep Rule Object Properties Overview** 82 | 83 | | Property | Type | Category | Purpose | Example | 84 | | :--- | :--- | :--- | :--- | :--- | 85 | | `pattern` | String or Object | Atomic | Matches AST node by code pattern. | `pattern: console.log($ARG)` | 86 | | `kind` | String | Atomic | Matches AST node by its kind name. | `kind: call_expression` | 87 | | `regex` | String | Atomic | Matches node's text by Rust regex. | `regex: ^[a-z]+$` | 88 | | `nthChild` | number, string, Object | Atomic | Matches nodes by their index within parent's children. | `nthChild: 1` | 89 | | `range` | RangeObject | Atomic | Matches node by character-based start/end positions. | `range: { start: { line: 0, column: 0 }, end: { line: 0, column: 10 } }` | 90 | | `inside` | Object | Relational | Target node must be inside node matching sub-rule. | `inside: { pattern: class $C { $$$ }, stopBy: end }` | 91 | | `has` | Object | Relational | Target node must have descendant matching sub-rule. | `has: { pattern: await $EXPR, stopBy: end }` | 92 | | `precedes` | Object | Relational | Target node must appear before node matching sub-rule. | `precedes: { pattern: return $VAL }` | 93 | | `follows` | Object | Relational | Target node must appear after node matching sub-rule. | `follows: { pattern: import $M from '$P' }` | 94 | | `all` | Array | Composite | Matches if all sub-rules match. | `all: [ { kind: call_expression }, { pattern: foo($A) } ]` | 95 | | `any` | Array | Composite | Matches if any sub-rules match. | `any: [ { pattern: foo() }, { pattern: bar() } ]` | 96 | | `not` | Object | Composite | Matches if sub-rule does not match. | `not: { pattern: console.log($ARG) }` | 97 | | `matches` | String | Composite | Matches if predefined utility rule matches. | `matches: my-utility-rule-id` | 98 | 99 | ## 3. Atomic Rules: Fundamental Matching Building Blocks 100 | 101 | Atomic rules match individual AST nodes based on their intrinsic properties. 102 | 103 | ### 3.1 `pattern`: String and Object Forms 104 | 105 | The `pattern` rule matches a single AST node based on a code pattern. 106 | * **String Pattern**: Directly matches using ast-grep's pattern syntax with metavariables. 107 | * Example: `pattern: console.log($ARG)` 108 | * **Object Pattern**: Offers granular control for ambiguous patterns or specific contexts. 109 | * `selector`: Pinpoints a specific part of the parsed pattern to match. 110 | ```yaml 111 | pattern: 112 | selector: field_definition 113 | context: class { $F } 114 | ``` 115 | 116 | * `context`: Provides surrounding code context for correct parsing. 117 | * `strictness`: Modifies the pattern's matching algorithm (`cst`, `smart`, `ast`, `relaxed`, `signature`). 118 | ```yaml 119 | pattern: 120 | context: foo($BAR) 121 | strictness: relaxed 122 | ``` 123 | 124 | 125 | ### 3.2 `kind`: Matching by Node Type 126 | 127 | The `kind` rule matches an AST node by its `tree_sitter_node_kind` name, derived from the language's Tree-sitter grammar. Useful for targeting constructs like `call_expression` or `function_declaration`. 128 | * Example: `kind: call_expression` 129 | 130 | ### 3.3 `regex`: Text-Based Node Matching 131 | 132 | The `regex` rule matches the entire text content of an AST node using a Rust regular expression. It's not a "positive" rule, meaning it matches any node whose text satisfies the regex, regardless of its structural kind. 133 | 134 | ### 3.4 `nthChild`: Positional Node Matching 135 | 136 | The `nthChild` rule finds nodes by their 1-based index within their parent's children list, counting only named nodes by default. 137 | * `number`: Matches the exact nth child. Example: `nthChild: 1` 138 | * `string`: Matches positions using An+B formula. Example: `2n+1` 139 | * `Object`: Provides granular control: 140 | * `position`: `number` or An+B string. 141 | * `reverse`: `true` to count from the end. 142 | * `ofRule`: An ast-grep rule to filter the sibling list before counting. 143 | 144 | ### 3.5 `range`: Position-Based Node Matching 145 | 146 | The `range` rule matches an AST node based on its character-based start and end positions. A `RangeObject` defines `start` and `end` fields, each with 0-based `line` and `column`. `start` is inclusive, `end` is exclusive. 147 | 148 | ## 4. Relational Rules: Contextual and Hierarchical Matching 149 | 150 | Relational rules filter targets based on their position relative to other AST nodes. They can include `stopBy` and `field` options. 151 | 152 | **** 153 | 154 | ### 4.1 `inside`: Matching Within a Parent Node 155 | 156 | Requires the target node to be inside another node matching the `inside` sub-rule. 157 | * Example: 158 | 159 | ```yaml 160 | inside: 161 | pattern: class $C { $$$ } 162 | stopBy: end 163 | ``` 164 | 165 | ### 4.2 `has`: Matching with a Descendant Node 166 | 167 | Requires the target node to have a descendant node matching the `has` sub-rule. 168 | * Example: 169 | ```yaml 170 | has: 171 | pattern: await $EXPR 172 | stopBy: end 173 | ``` 174 | 175 | ### 4.3 `precedes` and `follows`: Sequential Node Matching 176 | 177 | * `precedes`: Target node must appear before a node matching the `precedes` sub-rule. 178 | * `follows`: Target node must appear after a node matching the `follows` sub-rule. 179 | 180 | Both include `stopBy` but not `field`. 181 | 182 | ### 4.4 `stopBy` and `field`: Refining Relational Searches 183 | 184 | * `stopBy`: Controls search termination for relational rules. 185 | * `"neighbor"` (default): Stops when immediate surrounding node doesn't match. 186 | * `"end"`: Searches to the end of the direction (root for `inside`, leaf for `has`). 187 | * `Rule object`: Stops when a surrounding node matches the provided rule (inclusive). 188 | * `field`: Specifies a sub-node within the target node that should match the relational rule. Only for `inside` and `has`. 189 | 190 | When you are not sure, always use `stopBy: end` to ensure the search goes to the end of the direction. 191 | 192 | ## 5. Composite Rules: Logical Combination of Conditions 193 | 194 | Composite rules combine atomic and relational rules using logical operations. 195 | 196 | ### 5.1 `all`: Conjunction (AND) of Rules 197 | 198 | Matches a node only if all sub-rules in the list match. Guarantees order of rule matching, important for metavariables. 199 | * Example: 200 | ```yaml 201 | all: 202 | - kind: call_expression 203 | - pattern: console.log($ARG) 204 | ``` 205 | 206 | 207 | ### 5.2 `any`: Disjunction (OR) of Rules 208 | 209 | Matches a node if any sub-rules in the list match. 210 | * Example: 211 | ```yaml 212 | any: 213 | - pattern: console.log($ARG) 214 | - pattern: console.warn($ARG) 215 | - pattern: console.error($ARG) 216 | ``` 217 | 218 | 219 | ### 5.3 `not`: Negation (NOT) of a Rule 220 | 221 | Matches a node if the single sub-rule does not match. 222 | * Example: 223 | ```yaml 224 | not: 225 | pattern: console.log($ARG) 226 | ``` 227 | 228 | 229 | ### 5.4 `matches`: Rule Reuse and Utility Rules 230 | 231 | Takes a rule-id string, matching if the referenced utility rule matches. Enables rule reuse and recursive rules. 232 | 233 | ## 6. Metavariables: Dynamic Content Matching 234 | 235 | Metavariables are placeholders in patterns to match dynamic content in the AST. 236 | 237 | ### 6.1 `$VAR`: Single Named Node Capture 238 | 239 | Captures a single named node in the AST. 240 | * **Valid**: `$META`, `$META_VAR`, `$_` 241 | * **Invalid**: `$invalid`, `$123`, `$KEBAB-CASE` 242 | * **Example**: `console.log($GREETING)` matches `console.log('Hello World')`. 243 | * **Reuse**: `$A == $A` matches `a == a` but not `a == b`. 244 | 245 | ### 6.2 `$$VAR`: Single Unnamed Node Capture 246 | 247 | Captures a single unnamed node (e.g., operators, punctuation). 248 | * **Example**: To match the operator in `a + b`, use `$$OP`. 249 | ```yaml 250 | rule: 251 | kind: binary_expression 252 | has: 253 | field: operator 254 | pattern: $$OP 255 | ``` 256 | 257 | 258 | ### 6.3 `$$$MULTI_META_VARIABLE`: Multi-Node Capture 259 | 260 | Matches zero or more AST nodes (non-greedy). Useful for variable numbers of arguments or statements. 261 | * **Example**: `console.log($$$)` matches `console.log()`, `console.log('hello')`, and `console.log('debug:', key, value)`. 262 | * **Example**: `function $FUNC($$$ARGS) { $$$ }` matches functions with varying parameters/statements. 263 | 264 | ### 6.4 Non-Capturing Metavariables (`_VAR`) 265 | 266 | Metavariables starting with an underscore (`_`) are not captured. They can match different content even if named identically, optimizing performance. 267 | * **Example**: `$_FUNC($_FUNC)` matches `test(a)` and `testFunc(1 + 1)`. 268 | 269 | ### 6.5 Important Considerations for Metavariable Detection 270 | 271 | * **Syntax Matching**: Only exact metavariable syntax (e.g., `$A`, `$$B`, `$$$C`) is recognized. 272 | * **Exclusive Content**: Metavariable text must be the only text within an AST node. 273 | * **Non-working**: `obj.on$EVENT`, `"Hello $WORLD"`, `a $OP b`, `$jq`. 274 | 275 | The ast-grep playground is useful for debugging patterns and visualizing metavariables. 276 | 277 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import subprocess 5 | import sys 6 | from typing import Any, List, Literal, Optional 7 | 8 | import yaml 9 | from mcp.server.fastmcp import FastMCP 10 | from pydantic import Field 11 | 12 | # Global variable for config path (will be set by parse_args_and_get_config) 13 | CONFIG_PATH = None 14 | 15 | def parse_args_and_get_config(): 16 | """Parse command-line arguments and determine config path.""" 17 | global CONFIG_PATH 18 | 19 | # Determine how the script was invoked 20 | prog = None 21 | if sys.argv[0].endswith('main.py'): 22 | # Direct execution: python main.py 23 | prog = 'python main.py' 24 | 25 | # Parse command-line arguments 26 | parser = argparse.ArgumentParser( 27 | prog=prog, 28 | description='ast-grep MCP Server - Provides structural code search capabilities via Model Context Protocol', 29 | epilog=''' 30 | environment variables: 31 | AST_GREP_CONFIG Path to sgconfig.yaml file (overridden by --config flag) 32 | 33 | For more information, see: https://github.com/ast-grep/ast-grep-mcp 34 | ''', 35 | formatter_class=argparse.RawDescriptionHelpFormatter 36 | ) 37 | parser.add_argument( 38 | '--config', 39 | type=str, 40 | metavar='PATH', 41 | help='Path to sgconfig.yaml file for customizing ast-grep behavior (language mappings, rule directories, etc.)' 42 | ) 43 | args = parser.parse_args() 44 | 45 | # Determine config path with precedence: --config flag > AST_GREP_CONFIG env > None 46 | if args.config: 47 | if not os.path.exists(args.config): 48 | print(f"Error: Config file '{args.config}' does not exist") 49 | sys.exit(1) 50 | CONFIG_PATH = args.config 51 | elif os.environ.get('AST_GREP_CONFIG'): 52 | env_config = os.environ.get('AST_GREP_CONFIG') 53 | if env_config and not os.path.exists(env_config): 54 | print(f"Error: Config file '{env_config}' specified in AST_GREP_CONFIG does not exist") 55 | sys.exit(1) 56 | CONFIG_PATH = env_config 57 | 58 | # Initialize FastMCP server 59 | mcp = FastMCP("ast-grep") 60 | 61 | DumpFormat = Literal["pattern", "cst", "ast"] 62 | 63 | def register_mcp_tools() -> None: 64 | @mcp.tool() 65 | def dump_syntax_tree( 66 | code: str = Field(description = "The code you need"), 67 | language: str = Field(description = f"The language of the code. Supported: {', '.join(get_supported_languages())}"), 68 | format: DumpFormat = Field(description = "Code dump format. Available values: pattern, ast, cst", default = "cst"), 69 | ) -> str: 70 | """ 71 | Dump code's syntax structure or dump a query's pattern structure. 72 | This is useful to discover correct syntax kind and syntax tree structure. Call it when debugging a rule. 73 | The tool requires three arguments: code, language and format. The first two are self-explanatory. 74 | `format` is the output format of the syntax tree. 75 | use `format=cst` to inspect the code's concrete syntax tree structure, useful to debug target code. 76 | use `format=pattern` to inspect how ast-grep interprets a pattern, useful to debug pattern rule. 77 | 78 | Internally calls: ast-grep run --pattern --lang --debug-query= 79 | """ 80 | result = run_ast_grep("run", ["--pattern", code, "--lang", language, f"--debug-query={format}"]) 81 | return result.stderr.strip() # type: ignore[no-any-return] 82 | 83 | @mcp.tool() 84 | def test_match_code_rule( 85 | code: str = Field(description = "The code to test against the rule"), 86 | yaml: str = Field(description = "The ast-grep YAML rule to search. It must have id, language, rule fields."), 87 | ) -> List[dict[str, Any]]: 88 | """ 89 | Test a code against an ast-grep YAML rule. 90 | This is useful to test a rule before using it in a project. 91 | 92 | Internally calls: ast-grep scan --inline-rules --json --stdin 93 | """ 94 | result = run_ast_grep("scan", ["--inline-rules", yaml, "--json", "--stdin"], input_text = code) 95 | matches = json.loads(result.stdout.strip()) 96 | if not matches: 97 | raise ValueError("No matches found for the given code and rule. Try adding `stopBy: end` to your inside/has rule.") 98 | return matches # type: ignore[no-any-return] 99 | 100 | @mcp.tool() 101 | def find_code( 102 | project_folder: str = Field(description = "The absolute path to the project folder. It must be absolute path."), 103 | pattern: str = Field(description = "The ast-grep pattern to search for. Note, the pattern must have valid AST structure."), 104 | language: str = Field(description = f"The language of the code. Supported: {', '.join(get_supported_languages())}. " 105 | "If not specified, will be auto-detected based on file extensions.", default = ""), 106 | max_results: int = Field(default = 0, description = "Maximum results to return"), 107 | output_format: str = Field(default = "text", description = "'text' or 'json'"), 108 | ) -> str | List[dict[str, Any]]: 109 | """ 110 | Find code in a project folder that matches the given ast-grep pattern. 111 | Pattern is good for simple and single-AST node result. 112 | For more complex usage, please use YAML by `find_code_by_rule`. 113 | 114 | Internally calls: ast-grep run --pattern [--json] 115 | 116 | Output formats: 117 | - text (default): Compact text format with file:line-range headers and complete match text 118 | Example: 119 | Found 2 matches: 120 | 121 | path/to/file.py:10-15 122 | def example_function(): 123 | # function body 124 | return result 125 | 126 | path/to/file.py:20-22 127 | def another_function(): 128 | pass 129 | 130 | - json: Full match objects with metadata including ranges, meta-variables, etc. 131 | 132 | The max_results parameter limits the number of complete matches returned (not individual lines). 133 | When limited, the header shows "Found X matches (showing first Y of Z)". 134 | 135 | Example usage: 136 | find_code(pattern="class $NAME", max_results=20) # Returns text format 137 | find_code(pattern="class $NAME", output_format="json") # Returns JSON with metadata 138 | """ 139 | if output_format not in ["text", "json"]: 140 | raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.") 141 | 142 | args = ["--pattern", pattern] 143 | if language: 144 | args.extend(["--lang", language]) 145 | 146 | # Always get JSON internally for accurate match limiting 147 | result = run_ast_grep("run", args + ["--json", project_folder]) 148 | matches = json.loads(result.stdout.strip() or "[]") 149 | 150 | # Apply max_results limit to complete matches 151 | total_matches = len(matches) 152 | if max_results and total_matches > max_results: 153 | matches = matches[:max_results] 154 | 155 | if output_format == "text": 156 | if not matches: 157 | return "No matches found" 158 | text_output = format_matches_as_text(matches) 159 | header = f"Found {len(matches)} matches" 160 | if max_results and total_matches > max_results: 161 | header += f" (showing first {max_results} of {total_matches})" 162 | return header + ":\n\n" + text_output 163 | return matches # type: ignore[no-any-return] 164 | 165 | @mcp.tool() 166 | def find_code_by_rule( 167 | project_folder: str = Field(description = "The absolute path to the project folder. It must be absolute path."), 168 | yaml: str = Field(description = "The ast-grep YAML rule to search. It must have id, language, rule fields."), 169 | max_results: int = Field(default = 0, description = "Maximum results to return"), 170 | output_format: str = Field(default = "text", description = "'text' or 'json'"), 171 | ) -> str | List[dict[str, Any]]: 172 | """ 173 | Find code using ast-grep's YAML rule in a project folder. 174 | YAML rule is more powerful than simple pattern and can perform complex search like find AST inside/having another AST. 175 | It is a more advanced search tool than the simple `find_code`. 176 | 177 | Tip: When using relational rules (inside/has), add `stopBy: end` to ensure complete traversal. 178 | 179 | Internally calls: ast-grep scan --inline-rules [--json] 180 | 181 | Output formats: 182 | - text (default): Compact text format with file:line-range headers and complete match text 183 | Example: 184 | Found 2 matches: 185 | 186 | src/models.py:45-52 187 | class UserModel: 188 | def __init__(self): 189 | self.id = None 190 | self.name = None 191 | 192 | src/views.py:12 193 | class SimpleView: pass 194 | 195 | - json: Full match objects with metadata including ranges, meta-variables, etc. 196 | 197 | The max_results parameter limits the number of complete matches returned (not individual lines). 198 | When limited, the header shows "Found X matches (showing first Y of Z)". 199 | 200 | Example usage: 201 | find_code_by_rule(yaml="id: x\\nlanguage: python\\nrule: {pattern: 'class $NAME'}", max_results=20) 202 | find_code_by_rule(yaml="...", output_format="json") # For full metadata 203 | """ 204 | if output_format not in ["text", "json"]: 205 | raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.") 206 | 207 | args = ["--inline-rules", yaml] 208 | 209 | # Always get JSON internally for accurate match limiting 210 | result = run_ast_grep("scan", args + ["--json", project_folder]) 211 | matches = json.loads(result.stdout.strip() or "[]") 212 | 213 | # Apply max_results limit to complete matches 214 | total_matches = len(matches) 215 | if max_results and total_matches > max_results: 216 | matches = matches[:max_results] 217 | 218 | if output_format == "text": 219 | if not matches: 220 | return "No matches found" 221 | text_output = format_matches_as_text(matches) 222 | header = f"Found {len(matches)} matches" 223 | if max_results and total_matches > max_results: 224 | header += f" (showing first {max_results} of {total_matches})" 225 | return header + ":\n\n" + text_output 226 | return matches # type: ignore[no-any-return] 227 | 228 | 229 | def format_matches_as_text(matches: List[dict]) -> str: 230 | """Convert JSON matches to LLM-friendly text format. 231 | 232 | Format: file:start-end followed by the complete match text. 233 | Matches are separated by blank lines for clarity. 234 | """ 235 | if not matches: 236 | return "" 237 | 238 | output_blocks = [] 239 | for m in matches: 240 | file_path = m.get('file', '') 241 | start_line = m.get('range', {}).get('start', {}).get('line', 0) + 1 242 | end_line = m.get('range', {}).get('end', {}).get('line', 0) + 1 243 | match_text = m.get('text', '').rstrip() 244 | 245 | # Format: filepath:start-end (or just :line for single-line matches) 246 | if start_line == end_line: 247 | header = f"{file_path}:{start_line}" 248 | else: 249 | header = f"{file_path}:{start_line}-{end_line}" 250 | 251 | output_blocks.append(f"{header}\n{match_text}") 252 | 253 | return '\n\n'.join(output_blocks) 254 | 255 | def get_supported_languages() -> List[str]: 256 | """Get all supported languages as a field description string.""" 257 | languages = [ # https://ast-grep.github.io/reference/languages.html 258 | "bash", "c", "cpp", "csharp", "css", "elixir", "go", "haskell", 259 | "html", "java", "javascript", "json", "jsx", "kotlin", "lua", 260 | "nix", "php", "python", "ruby", "rust", "scala", "solidity", 261 | "swift", "tsx", "typescript", "yaml" 262 | ] 263 | 264 | # Check for custom languages in config file 265 | # https://ast-grep.github.io/advanced/custom-language.html#register-language-in-sgconfig-yml 266 | if CONFIG_PATH and os.path.exists(CONFIG_PATH): 267 | try: 268 | with open(CONFIG_PATH, 'r') as f: 269 | config = yaml.safe_load(f) 270 | if config and 'customLanguages' in config: 271 | custom_langs = list(config['customLanguages'].keys()) 272 | languages += custom_langs 273 | except Exception: 274 | pass 275 | 276 | return sorted(set(languages)) 277 | 278 | def run_command(args: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess: 279 | try: 280 | # On Windows, if ast-grep is installed via npm, it's a batch file 281 | # that requires shell=True to execute properly 282 | use_shell = (sys.platform == "win32" and args[0] == "ast-grep") 283 | 284 | result = subprocess.run( 285 | args, 286 | capture_output=True, 287 | input=input_text, 288 | text=True, 289 | check=True, # Raises CalledProcessError if return code is non-zero 290 | shell=use_shell 291 | ) 292 | return result 293 | except subprocess.CalledProcessError as e: 294 | stderr_msg = e.stderr.strip() if e.stderr else "(no error output)" 295 | error_msg = f"Command {e.cmd} failed with exit code {e.returncode}: {stderr_msg}" 296 | raise RuntimeError(error_msg) from e 297 | except FileNotFoundError as e: 298 | error_msg = f"Command '{args[0]}' not found. Please ensure {args[0]} is installed and in PATH." 299 | raise RuntimeError(error_msg) from e 300 | 301 | def run_ast_grep(command:str, args: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess: 302 | if CONFIG_PATH: 303 | args = ["--config", CONFIG_PATH] + args 304 | return run_command(["ast-grep", command] + args, input_text) 305 | 306 | def run_mcp_server() -> None: 307 | """ 308 | Run the MCP server. 309 | This function is used to start the MCP server when this script is run directly. 310 | """ 311 | parse_args_and_get_config() # sets CONFIG_PATH 312 | register_mcp_tools() # tools defined *after* CONFIG_PATH is known 313 | mcp.run(transport="stdio") 314 | 315 | if __name__ == "__main__": 316 | run_mcp_server() 317 | -------------------------------------------------------------------------------- /tests/test_unit.py: -------------------------------------------------------------------------------- 1 | """Unit tests for ast-grep MCP server""" 2 | 3 | import json 4 | import os 5 | import subprocess 6 | import sys 7 | from unittest.mock import Mock, patch 8 | 9 | import pytest 10 | 11 | # Add the parent directory to the path 12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | 14 | 15 | # Mock FastMCP to disable decoration 16 | class MockFastMCP: 17 | """Mock FastMCP that returns functions unchanged""" 18 | 19 | def __init__(self, name): 20 | self.name = name 21 | self.tools = {} # Store registered tools 22 | 23 | def tool(self, **kwargs): 24 | """Decorator that returns the function unchanged""" 25 | 26 | def decorator(func): 27 | # Store the function for later retrieval 28 | self.tools[func.__name__] = func 29 | return func # Return original function without modification 30 | 31 | return decorator 32 | 33 | def run(self, **kwargs): 34 | """Mock run method""" 35 | pass 36 | 37 | 38 | # Mock the Field function to return the default value 39 | def mock_field(**kwargs): 40 | return kwargs.get("default") 41 | 42 | 43 | # Patch the imports before loading main 44 | with patch("mcp.server.fastmcp.FastMCP", MockFastMCP): 45 | with patch("pydantic.Field", mock_field): 46 | import main 47 | from main import ( 48 | format_matches_as_text, 49 | run_ast_grep, 50 | run_command, 51 | ) 52 | 53 | # Call register_mcp_tools to define the tool functions 54 | main.register_mcp_tools() 55 | 56 | # Extract the tool functions from the mocked mcp instance 57 | dump_syntax_tree = main.mcp.tools.get("dump_syntax_tree") 58 | find_code = main.mcp.tools.get("find_code") 59 | find_code_by_rule = main.mcp.tools.get("find_code_by_rule") 60 | match_code_rule = main.mcp.tools.get("test_match_code_rule") 61 | 62 | 63 | class TestDumpSyntaxTree: 64 | """Test the dump_syntax_tree function""" 65 | 66 | @patch("main.run_ast_grep") 67 | def test_dump_syntax_tree_cst(self, mock_run): 68 | """Test dumping CST format""" 69 | mock_result = Mock() 70 | mock_result.stderr = "ROOT@0..10" 71 | mock_run.return_value = mock_result 72 | 73 | result = dump_syntax_tree("const x = 1", "javascript", "cst") 74 | 75 | assert result == "ROOT@0..10" 76 | mock_run.assert_called_once_with( 77 | "run", 78 | ["--pattern", "const x = 1", "--lang", "javascript", "--debug-query=cst"], 79 | ) 80 | 81 | @patch("main.run_ast_grep") 82 | def test_dump_syntax_tree_pattern(self, mock_run): 83 | """Test dumping pattern format""" 84 | mock_result = Mock() 85 | mock_result.stderr = "pattern_node" 86 | mock_run.return_value = mock_result 87 | 88 | result = dump_syntax_tree("$VAR", "python", "pattern") 89 | 90 | assert result == "pattern_node" 91 | mock_run.assert_called_once_with( 92 | "run", ["--pattern", "$VAR", "--lang", "python", "--debug-query=pattern"] 93 | ) 94 | 95 | 96 | class TestTestMatchCodeRule: 97 | """Test the test_match_code_rule function""" 98 | 99 | @patch("main.run_ast_grep") 100 | def test_match_found(self, mock_run): 101 | """Test when matches are found""" 102 | mock_result = Mock() 103 | mock_result.stdout = '[{"text": "def foo(): pass"}]' 104 | mock_run.return_value = mock_result 105 | 106 | yaml_rule = """id: test 107 | language: python 108 | rule: 109 | pattern: 'def $NAME(): $$$' 110 | """ 111 | code = "def foo(): pass" 112 | 113 | result = match_code_rule(code, yaml_rule) 114 | 115 | assert result == [{"text": "def foo(): pass"}] 116 | mock_run.assert_called_once_with( 117 | "scan", ["--inline-rules", yaml_rule, "--json", "--stdin"], input_text=code 118 | ) 119 | 120 | @patch("main.run_ast_grep") 121 | def test_no_match(self, mock_run): 122 | """Test when no matches are found""" 123 | mock_result = Mock() 124 | mock_result.stdout = "[]" 125 | mock_run.return_value = mock_result 126 | 127 | yaml_rule = """id: test 128 | language: python 129 | rule: 130 | pattern: 'class $NAME' 131 | """ 132 | code = "def foo(): pass" 133 | 134 | with pytest.raises(ValueError, match="No matches found"): 135 | match_code_rule(code, yaml_rule) 136 | 137 | 138 | class TestFindCode: 139 | """Test the find_code function""" 140 | 141 | @patch("main.run_ast_grep") 142 | def test_text_format_with_results(self, mock_run): 143 | """Test text format output with results""" 144 | mock_result = Mock() 145 | mock_matches = [ 146 | {"text": "def foo():\n pass", "file": "file.py", 147 | "range": {"start": {"line": 0}, "end": {"line": 1}}}, 148 | {"text": "def bar():\n return", "file": "file.py", 149 | "range": {"start": {"line": 4}, "end": {"line": 5}}} 150 | ] 151 | mock_result.stdout = json.dumps(mock_matches) 152 | mock_run.return_value = mock_result 153 | 154 | result = find_code( 155 | project_folder="/test/path", 156 | pattern="def $NAME():", 157 | language="python", 158 | output_format="text", 159 | ) 160 | 161 | assert "Found 2 matches:" in result 162 | assert "def foo():" in result 163 | assert "def bar():" in result 164 | assert "file.py:1-2" in result 165 | assert "file.py:5-6" in result 166 | mock_run.assert_called_once_with( 167 | "run", ["--pattern", "def $NAME():", "--lang", "python", "--json", "/test/path"] 168 | ) 169 | 170 | @patch("main.run_ast_grep") 171 | def test_text_format_no_results(self, mock_run): 172 | """Test text format output with no results""" 173 | mock_result = Mock() 174 | mock_result.stdout = "[]" 175 | mock_run.return_value = mock_result 176 | 177 | result = find_code( 178 | project_folder="/test/path", pattern="nonexistent", output_format="text" 179 | ) 180 | 181 | assert result == "No matches found" 182 | mock_run.assert_called_once_with( 183 | "run", ["--pattern", "nonexistent", "--json", "/test/path"] 184 | ) 185 | 186 | @patch("main.run_ast_grep") 187 | def test_text_format_with_max_results(self, mock_run): 188 | """Test text format with max_results limit""" 189 | mock_result = Mock() 190 | mock_matches = [ 191 | {"text": "match1", "file": "f.py", "range": {"start": {"line": 0}, "end": {"line": 0}}}, 192 | {"text": "match2", "file": "f.py", "range": {"start": {"line": 1}, "end": {"line": 1}}}, 193 | {"text": "match3", "file": "f.py", "range": {"start": {"line": 2}, "end": {"line": 2}}}, 194 | {"text": "match4", "file": "f.py", "range": {"start": {"line": 3}, "end": {"line": 3}}}, 195 | ] 196 | mock_result.stdout = json.dumps(mock_matches) 197 | mock_run.return_value = mock_result 198 | 199 | result = find_code( 200 | project_folder="/test/path", 201 | pattern="pattern", 202 | max_results=2, 203 | output_format="text", 204 | ) 205 | 206 | assert "Found 2 matches (showing first 2 of 4):" in result 207 | assert "match1" in result 208 | assert "match2" in result 209 | assert "match3" not in result 210 | 211 | @patch("main.run_ast_grep") 212 | def test_json_format(self, mock_run): 213 | """Test JSON format output""" 214 | mock_result = Mock() 215 | mock_matches = [ 216 | {"text": "def foo():", "file": "test.py"}, 217 | {"text": "def bar():", "file": "test.py"}, 218 | ] 219 | mock_result.stdout = json.dumps(mock_matches) 220 | mock_run.return_value = mock_result 221 | 222 | result = find_code( 223 | project_folder="/test/path", pattern="def $NAME():", output_format="json" 224 | ) 225 | 226 | assert result == mock_matches 227 | mock_run.assert_called_once_with( 228 | "run", ["--pattern", "def $NAME():", "--json", "/test/path"] 229 | ) 230 | 231 | @patch("main.run_ast_grep") 232 | def test_json_format_with_max_results(self, mock_run): 233 | """Test JSON format with max_results limit""" 234 | mock_result = Mock() 235 | mock_matches = [{"text": "match1"}, {"text": "match2"}, {"text": "match3"}] 236 | mock_result.stdout = json.dumps(mock_matches) 237 | mock_run.return_value = mock_result 238 | 239 | result = find_code( 240 | project_folder="/test/path", 241 | pattern="pattern", 242 | max_results=2, 243 | output_format="json", 244 | ) 245 | 246 | assert len(result) == 2 247 | assert result[0]["text"] == "match1" 248 | assert result[1]["text"] == "match2" 249 | 250 | def test_invalid_output_format(self): 251 | """Test with invalid output format""" 252 | with pytest.raises(ValueError, match="Invalid output_format"): 253 | find_code( 254 | project_folder="/test/path", pattern="pattern", output_format="invalid" 255 | ) 256 | 257 | 258 | class TestFindCodeByRule: 259 | """Test the find_code_by_rule function""" 260 | 261 | @patch("main.run_ast_grep") 262 | def test_text_format_with_results(self, mock_run): 263 | """Test text format output with results""" 264 | mock_result = Mock() 265 | mock_matches = [ 266 | {"text": "class Foo:\n pass", "file": "file.py", 267 | "range": {"start": {"line": 0}, "end": {"line": 1}}}, 268 | {"text": "class Bar:\n pass", "file": "file.py", 269 | "range": {"start": {"line": 9}, "end": {"line": 10}}} 270 | ] 271 | mock_result.stdout = json.dumps(mock_matches) 272 | mock_run.return_value = mock_result 273 | 274 | yaml_rule = """id: test 275 | language: python 276 | rule: 277 | pattern: 'class $NAME' 278 | """ 279 | 280 | result = find_code_by_rule( 281 | project_folder="/test/path", yaml=yaml_rule, output_format="text" 282 | ) 283 | 284 | assert "Found 2 matches:" in result 285 | assert "class Foo:" in result 286 | assert "class Bar:" in result 287 | assert "file.py:1-2" in result 288 | assert "file.py:10-11" in result 289 | mock_run.assert_called_once_with( 290 | "scan", ["--inline-rules", yaml_rule, "--json", "/test/path"] 291 | ) 292 | 293 | @patch("main.run_ast_grep") 294 | def test_json_format(self, mock_run): 295 | """Test JSON format output""" 296 | mock_result = Mock() 297 | mock_matches = [{"text": "class Foo:", "file": "test.py"}] 298 | mock_result.stdout = json.dumps(mock_matches) 299 | mock_run.return_value = mock_result 300 | 301 | yaml_rule = """id: test 302 | language: python 303 | rule: 304 | pattern: 'class $NAME' 305 | """ 306 | 307 | result = find_code_by_rule( 308 | project_folder="/test/path", yaml=yaml_rule, output_format="json" 309 | ) 310 | 311 | assert result == mock_matches 312 | mock_run.assert_called_once_with( 313 | "scan", ["--inline-rules", yaml_rule, "--json", "/test/path"] 314 | ) 315 | 316 | 317 | class TestRunCommand: 318 | """Test the run_command function""" 319 | 320 | @patch("subprocess.run") 321 | def test_successful_command(self, mock_run): 322 | """Test successful command execution""" 323 | mock_result = Mock() 324 | mock_result.returncode = 0 325 | mock_result.stdout = "output" 326 | mock_run.return_value = mock_result 327 | 328 | result = run_command(["echo", "test"]) 329 | 330 | assert result.stdout == "output" 331 | mock_run.assert_called_once_with( 332 | ["echo", "test"], capture_output=True, input=None, text=True, check=True, shell=False 333 | ) 334 | 335 | @patch("subprocess.run") 336 | def test_command_failure(self, mock_run): 337 | """Test command execution failure""" 338 | mock_run.side_effect = subprocess.CalledProcessError( 339 | 1, ["false"], stderr="error message" 340 | ) 341 | 342 | with pytest.raises(RuntimeError, match="failed with exit code 1"): 343 | run_command(["false"]) 344 | 345 | @patch("subprocess.run") 346 | def test_command_not_found(self, mock_run): 347 | """Test when command is not found""" 348 | mock_run.side_effect = FileNotFoundError() 349 | 350 | with pytest.raises(RuntimeError, match="not found"): 351 | run_command(["nonexistent"]) 352 | 353 | 354 | class TestFormatMatchesAsText: 355 | """Test the format_matches_as_text helper function""" 356 | 357 | def test_empty_matches(self): 358 | """Test with empty matches list""" 359 | result = format_matches_as_text([]) 360 | assert result == "" 361 | 362 | def test_single_line_match(self): 363 | """Test formatting a single-line match""" 364 | matches = [ 365 | { 366 | "text": "const x = 1", 367 | "file": "test.js", 368 | "range": {"start": {"line": 4}, "end": {"line": 4}} 369 | } 370 | ] 371 | result = format_matches_as_text(matches) 372 | assert result == "test.js:5\nconst x = 1" 373 | 374 | def test_multi_line_match(self): 375 | """Test formatting a multi-line match""" 376 | matches = [ 377 | { 378 | "text": "def foo():\n return 42", 379 | "file": "test.py", 380 | "range": {"start": {"line": 9}, "end": {"line": 10}} 381 | } 382 | ] 383 | result = format_matches_as_text(matches) 384 | assert result == "test.py:10-11\ndef foo():\n return 42" 385 | 386 | def test_multiple_matches(self): 387 | """Test formatting multiple matches""" 388 | matches = [ 389 | { 390 | "text": "match1", 391 | "file": "file1.py", 392 | "range": {"start": {"line": 0}, "end": {"line": 0}} 393 | }, 394 | { 395 | "text": "match2\nline2", 396 | "file": "file2.py", 397 | "range": {"start": {"line": 5}, "end": {"line": 6}} 398 | } 399 | ] 400 | result = format_matches_as_text(matches) 401 | expected = "file1.py:1\nmatch1\n\nfile2.py:6-7\nmatch2\nline2" 402 | assert result == expected 403 | 404 | 405 | class TestRunAstGrep: 406 | """Test the run_ast_grep function""" 407 | 408 | @patch("main.run_command") 409 | @patch("main.CONFIG_PATH", None) 410 | def test_without_config(self, mock_run): 411 | """Test running ast-grep without config""" 412 | mock_result = Mock() 413 | mock_run.return_value = mock_result 414 | 415 | result = run_ast_grep("run", ["--pattern", "test"]) 416 | 417 | assert result == mock_result 418 | mock_run.assert_called_once_with(["ast-grep", "run", "--pattern", "test"], None) 419 | 420 | @patch("main.run_command") 421 | @patch("main.CONFIG_PATH", "/path/to/config.yaml") 422 | def test_with_config(self, mock_run): 423 | """Test running ast-grep with config""" 424 | mock_result = Mock() 425 | mock_run.return_value = mock_result 426 | 427 | result = run_ast_grep("scan", ["--inline-rules", "rule"]) 428 | 429 | assert result == mock_result 430 | mock_run.assert_called_once_with( 431 | [ 432 | "ast-grep", 433 | "scan", 434 | "--config", 435 | "/path/to/config.yaml", 436 | "--inline-rules", 437 | "rule", 438 | ], 439 | None, 440 | ) 441 | 442 | 443 | if __name__ == "__main__": 444 | pytest.main([__file__, "-v"]) 445 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | 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" } 10 | wheels = [ 11 | { 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" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | ] 22 | 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" } 23 | wheels = [ 24 | { 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" }, 25 | ] 26 | 27 | [[package]] 28 | name = "certifi" 29 | version = "2025.1.31" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, 34 | ] 35 | 36 | [[package]] 37 | name = "click" 38 | version = "8.1.8" 39 | source = { registry = "https://pypi.org/simple" } 40 | dependencies = [ 41 | { name = "colorama", marker = "sys_platform == 'win32'" }, 42 | ] 43 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, 46 | ] 47 | 48 | [[package]] 49 | name = "colorama" 50 | version = "0.4.6" 51 | source = { registry = "https://pypi.org/simple" } 52 | 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" } 53 | wheels = [ 54 | { 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" }, 55 | ] 56 | 57 | [[package]] 58 | name = "coverage" 59 | version = "7.10.2" 60 | source = { registry = "https://pypi.org/simple" } 61 | sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } 62 | wheels = [ 63 | { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, 64 | { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, 65 | { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, 66 | { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, 67 | { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, 68 | { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, 69 | { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, 70 | { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, 71 | { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, 72 | { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, 73 | { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, 74 | { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, 75 | { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, 76 | { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, 77 | { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, 78 | { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, 79 | { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, 80 | { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, 81 | { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, 82 | { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, 83 | { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, 84 | { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, 85 | { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, 86 | { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, 87 | { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, 88 | { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, 89 | { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, 90 | { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, 91 | { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, 92 | { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, 93 | { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, 94 | { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, 95 | { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, 96 | { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, 97 | { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, 98 | { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, 99 | { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, 100 | { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, 101 | { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, 102 | { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, 103 | { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, 104 | { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, 105 | { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, 106 | { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, 107 | { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, 108 | ] 109 | 110 | [[package]] 111 | name = "h11" 112 | version = "0.14.0" 113 | source = { registry = "https://pypi.org/simple" } 114 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } 115 | wheels = [ 116 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, 117 | ] 118 | 119 | [[package]] 120 | name = "httpcore" 121 | version = "1.0.7" 122 | source = { registry = "https://pypi.org/simple" } 123 | dependencies = [ 124 | { name = "certifi" }, 125 | { name = "h11" }, 126 | ] 127 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196, upload-time = "2024-11-15T12:30:47.531Z" } 128 | wheels = [ 129 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, 130 | ] 131 | 132 | [[package]] 133 | name = "httpx" 134 | version = "0.28.1" 135 | source = { registry = "https://pypi.org/simple" } 136 | dependencies = [ 137 | { name = "anyio" }, 138 | { name = "certifi" }, 139 | { name = "httpcore" }, 140 | { name = "idna" }, 141 | ] 142 | 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" } 143 | wheels = [ 144 | { 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" }, 145 | ] 146 | 147 | [[package]] 148 | name = "httpx-sse" 149 | version = "0.4.0" 150 | source = { registry = "https://pypi.org/simple" } 151 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } 152 | wheels = [ 153 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, 154 | ] 155 | 156 | [[package]] 157 | name = "idna" 158 | version = "3.10" 159 | source = { registry = "https://pypi.org/simple" } 160 | 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" } 161 | wheels = [ 162 | { 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" }, 163 | ] 164 | 165 | [[package]] 166 | name = "iniconfig" 167 | version = "2.1.0" 168 | source = { registry = "https://pypi.org/simple" } 169 | 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" } 170 | wheels = [ 171 | { 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" }, 172 | ] 173 | 174 | [[package]] 175 | name = "markdown-it-py" 176 | version = "3.0.0" 177 | source = { registry = "https://pypi.org/simple" } 178 | dependencies = [ 179 | { name = "mdurl" }, 180 | ] 181 | 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" } 182 | wheels = [ 183 | { 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" }, 184 | ] 185 | 186 | [[package]] 187 | name = "mcp" 188 | version = "1.6.0" 189 | source = { registry = "https://pypi.org/simple" } 190 | dependencies = [ 191 | { name = "anyio" }, 192 | { name = "httpx" }, 193 | { name = "httpx-sse" }, 194 | { name = "pydantic" }, 195 | { name = "pydantic-settings" }, 196 | { name = "sse-starlette" }, 197 | { name = "starlette" }, 198 | { name = "uvicorn" }, 199 | ] 200 | sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031, upload-time = "2025-03-27T16:46:32.336Z" } 201 | wheels = [ 202 | { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077, upload-time = "2025-03-27T16:46:29.919Z" }, 203 | ] 204 | 205 | [package.optional-dependencies] 206 | cli = [ 207 | { name = "python-dotenv" }, 208 | { name = "typer" }, 209 | ] 210 | 211 | [[package]] 212 | name = "mdurl" 213 | version = "0.1.2" 214 | source = { registry = "https://pypi.org/simple" } 215 | 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" } 216 | wheels = [ 217 | { 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" }, 218 | ] 219 | 220 | [[package]] 221 | name = "mypy" 222 | version = "1.17.1" 223 | source = { registry = "https://pypi.org/simple" } 224 | dependencies = [ 225 | { name = "mypy-extensions" }, 226 | { name = "pathspec" }, 227 | { name = "typing-extensions" }, 228 | ] 229 | sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } 230 | wheels = [ 231 | { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, 232 | { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, 233 | { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, 234 | { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, 235 | { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, 236 | { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, 237 | { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, 238 | { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, 239 | { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, 240 | { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, 241 | { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, 242 | { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, 243 | { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, 244 | ] 245 | 246 | [[package]] 247 | name = "mypy-extensions" 248 | version = "1.1.0" 249 | source = { registry = "https://pypi.org/simple" } 250 | 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" } 251 | wheels = [ 252 | { 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" }, 253 | ] 254 | 255 | [[package]] 256 | name = "packaging" 257 | version = "25.0" 258 | source = { registry = "https://pypi.org/simple" } 259 | 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" } 260 | wheels = [ 261 | { 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" }, 262 | ] 263 | 264 | [[package]] 265 | name = "pathspec" 266 | version = "0.12.1" 267 | source = { registry = "https://pypi.org/simple" } 268 | 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" } 269 | wheels = [ 270 | { 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" }, 271 | ] 272 | 273 | [[package]] 274 | name = "pluggy" 275 | version = "1.6.0" 276 | source = { registry = "https://pypi.org/simple" } 277 | 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" } 278 | wheels = [ 279 | { 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" }, 280 | ] 281 | 282 | [[package]] 283 | name = "pydantic" 284 | version = "2.11.1" 285 | source = { registry = "https://pypi.org/simple" } 286 | dependencies = [ 287 | { name = "annotated-types" }, 288 | { name = "pydantic-core" }, 289 | { name = "typing-extensions" }, 290 | { name = "typing-inspection" }, 291 | ] 292 | sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817, upload-time = "2025-03-28T21:14:58.347Z" } 293 | wheels = [ 294 | { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648, upload-time = "2025-03-28T21:14:55.856Z" }, 295 | ] 296 | 297 | [[package]] 298 | name = "pydantic-core" 299 | version = "2.33.0" 300 | source = { registry = "https://pypi.org/simple" } 301 | dependencies = [ 302 | { name = "typing-extensions" }, 303 | ] 304 | sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080, upload-time = "2025-03-26T20:30:05.906Z" } 305 | wheels = [ 306 | { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214, upload-time = "2025-03-26T20:27:56.197Z" }, 307 | { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338, upload-time = "2025-03-26T20:27:57.876Z" }, 308 | { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913, upload-time = "2025-03-26T20:27:59.719Z" }, 309 | { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046, upload-time = "2025-03-26T20:28:01.583Z" }, 310 | { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097, upload-time = "2025-03-26T20:28:03.437Z" }, 311 | { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062, upload-time = "2025-03-26T20:28:05.498Z" }, 312 | { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487, upload-time = "2025-03-26T20:28:07.879Z" }, 313 | { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382, upload-time = "2025-03-26T20:28:09.651Z" }, 314 | { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473, upload-time = "2025-03-26T20:28:11.69Z" }, 315 | { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468, upload-time = "2025-03-26T20:28:13.651Z" }, 316 | { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716, upload-time = "2025-03-26T20:28:16.105Z" }, 317 | { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450, upload-time = "2025-03-26T20:28:18.252Z" }, 318 | { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092, upload-time = "2025-03-26T20:28:20.129Z" }, 319 | { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367, upload-time = "2025-03-26T20:28:22.498Z" }, 320 | { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331, upload-time = "2025-03-26T20:28:25.004Z" }, 321 | { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653, upload-time = "2025-03-26T20:28:27.02Z" }, 322 | { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234, upload-time = "2025-03-26T20:28:29.237Z" }, 323 | ] 324 | 325 | [[package]] 326 | name = "pydantic-settings" 327 | version = "2.8.1" 328 | source = { registry = "https://pypi.org/simple" } 329 | dependencies = [ 330 | { name = "pydantic" }, 331 | { name = "python-dotenv" }, 332 | ] 333 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } 334 | wheels = [ 335 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, 336 | ] 337 | 338 | [[package]] 339 | name = "pygments" 340 | version = "2.19.1" 341 | source = { registry = "https://pypi.org/simple" } 342 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } 343 | wheels = [ 344 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, 345 | ] 346 | 347 | [[package]] 348 | name = "pytest" 349 | version = "8.4.1" 350 | source = { registry = "https://pypi.org/simple" } 351 | dependencies = [ 352 | { name = "colorama", marker = "sys_platform == 'win32'" }, 353 | { name = "iniconfig" }, 354 | { name = "packaging" }, 355 | { name = "pluggy" }, 356 | { name = "pygments" }, 357 | ] 358 | 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" } 359 | wheels = [ 360 | { 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" }, 361 | ] 362 | 363 | [[package]] 364 | name = "pytest-cov" 365 | version = "6.2.1" 366 | source = { registry = "https://pypi.org/simple" } 367 | dependencies = [ 368 | { name = "coverage" }, 369 | { name = "pluggy" }, 370 | { name = "pytest" }, 371 | ] 372 | sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } 373 | wheels = [ 374 | { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, 375 | ] 376 | 377 | [[package]] 378 | name = "pytest-mock" 379 | version = "3.14.1" 380 | source = { registry = "https://pypi.org/simple" } 381 | dependencies = [ 382 | { name = "pytest" }, 383 | ] 384 | sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } 385 | wheels = [ 386 | { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, 387 | ] 388 | 389 | [[package]] 390 | name = "python-dotenv" 391 | version = "1.1.0" 392 | source = { registry = "https://pypi.org/simple" } 393 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } 394 | wheels = [ 395 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, 396 | ] 397 | 398 | [[package]] 399 | name = "pyyaml" 400 | version = "6.0.2" 401 | source = { registry = "https://pypi.org/simple" } 402 | 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" } 403 | wheels = [ 404 | { 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" }, 405 | { 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" }, 406 | { 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" }, 407 | { 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" }, 408 | { 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" }, 409 | { 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" }, 410 | { 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" }, 411 | { 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" }, 412 | { 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" }, 413 | ] 414 | 415 | [[package]] 416 | name = "rich" 417 | version = "14.0.0" 418 | source = { registry = "https://pypi.org/simple" } 419 | dependencies = [ 420 | { name = "markdown-it-py" }, 421 | { name = "pygments" }, 422 | ] 423 | 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" } 424 | wheels = [ 425 | { 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" }, 426 | ] 427 | 428 | [[package]] 429 | name = "ruff" 430 | version = "0.12.8" 431 | source = { registry = "https://pypi.org/simple" } 432 | sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } 433 | wheels = [ 434 | { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, 435 | { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, 436 | { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, 437 | { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, 438 | { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, 439 | { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, 440 | { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, 441 | { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, 442 | { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, 443 | { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, 444 | { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, 445 | { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, 446 | { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, 447 | { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, 448 | { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, 449 | { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, 450 | { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, 451 | ] 452 | 453 | [[package]] 454 | name = "sg-mcp" 455 | version = "0.1.0" 456 | source = { virtual = "." } 457 | dependencies = [ 458 | { name = "mcp", extra = ["cli"] }, 459 | { name = "pydantic" }, 460 | { name = "pyyaml" }, 461 | ] 462 | 463 | [package.optional-dependencies] 464 | dev = [ 465 | { name = "mypy" }, 466 | { name = "pytest" }, 467 | { name = "pytest-cov" }, 468 | { name = "pytest-mock" }, 469 | { name = "ruff" }, 470 | { name = "types-pyyaml" }, 471 | ] 472 | 473 | [package.metadata] 474 | requires-dist = [ 475 | { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, 476 | { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13.0" }, 477 | { name = "pydantic", specifier = ">=2.11.0" }, 478 | { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, 479 | { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, 480 | { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, 481 | { name = "pyyaml", specifier = ">=6.0.2" }, 482 | { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7.0" }, 483 | { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250809" }, 484 | ] 485 | provides-extras = ["dev"] 486 | 487 | [[package]] 488 | name = "shellingham" 489 | version = "1.5.4" 490 | source = { registry = "https://pypi.org/simple" } 491 | 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" } 492 | wheels = [ 493 | { 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" }, 494 | ] 495 | 496 | [[package]] 497 | name = "sniffio" 498 | version = "1.3.1" 499 | source = { registry = "https://pypi.org/simple" } 500 | 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" } 501 | wheels = [ 502 | { 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" }, 503 | ] 504 | 505 | [[package]] 506 | name = "sse-starlette" 507 | version = "2.2.1" 508 | source = { registry = "https://pypi.org/simple" } 509 | dependencies = [ 510 | { name = "anyio" }, 511 | { name = "starlette" }, 512 | ] 513 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376, upload-time = "2024-12-25T09:09:30.616Z" } 514 | wheels = [ 515 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload-time = "2024-12-25T09:09:26.761Z" }, 516 | ] 517 | 518 | [[package]] 519 | name = "starlette" 520 | version = "0.46.1" 521 | source = { registry = "https://pypi.org/simple" } 522 | dependencies = [ 523 | { name = "anyio" }, 524 | ] 525 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102, upload-time = "2025-03-08T10:55:34.504Z" } 526 | wheels = [ 527 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995, upload-time = "2025-03-08T10:55:32.662Z" }, 528 | ] 529 | 530 | [[package]] 531 | name = "typer" 532 | version = "0.15.2" 533 | source = { registry = "https://pypi.org/simple" } 534 | dependencies = [ 535 | { name = "click" }, 536 | { name = "rich" }, 537 | { name = "shellingham" }, 538 | { name = "typing-extensions" }, 539 | ] 540 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload-time = "2025-02-27T19:17:34.807Z" } 541 | wheels = [ 542 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload-time = "2025-02-27T19:17:32.111Z" }, 543 | ] 544 | 545 | [[package]] 546 | name = "types-pyyaml" 547 | version = "6.0.12.20250809" 548 | source = { registry = "https://pypi.org/simple" } 549 | sdist = { url = "https://files.pythonhosted.org/packages/36/21/52ffdbddea3c826bc2758d811ccd7f766912de009c5cf096bd5ebba44680/types_pyyaml-6.0.12.20250809.tar.gz", hash = "sha256:af4a1aca028f18e75297da2ee0da465f799627370d74073e96fee876524f61b5", size = 17385, upload-time = "2025-08-09T03:14:34.867Z" } 550 | wheels = [ 551 | { url = "https://files.pythonhosted.org/packages/35/3e/0346d09d6e338401ebf406f12eaf9d0b54b315b86f1ec29e34f1a0aedae9/types_pyyaml-6.0.12.20250809-py3-none-any.whl", hash = "sha256:032b6003b798e7de1a1ddfeefee32fac6486bdfe4845e0ae0e7fb3ee4512b52f", size = 20277, upload-time = "2025-08-09T03:14:34.055Z" }, 552 | ] 553 | 554 | [[package]] 555 | name = "typing-extensions" 556 | version = "4.13.0" 557 | source = { registry = "https://pypi.org/simple" } 558 | sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520, upload-time = "2025-03-26T03:49:41.628Z" } 559 | wheels = [ 560 | { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683, upload-time = "2025-03-26T03:49:40.35Z" }, 561 | ] 562 | 563 | [[package]] 564 | name = "typing-inspection" 565 | version = "0.4.0" 566 | source = { registry = "https://pypi.org/simple" } 567 | dependencies = [ 568 | { name = "typing-extensions" }, 569 | ] 570 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } 571 | wheels = [ 572 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, 573 | ] 574 | 575 | [[package]] 576 | name = "uvicorn" 577 | version = "0.34.0" 578 | source = { registry = "https://pypi.org/simple" } 579 | dependencies = [ 580 | { name = "click" }, 581 | { name = "h11" }, 582 | ] 583 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } 584 | wheels = [ 585 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, 586 | ] 587 | --------------------------------------------------------------------------------