cli-mcp-server 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # PyCharm
13
+ .idea/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Mladen
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.
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.3
2
+ Name: cli-mcp-server
3
+ Version: 0.1.2
4
+ Summary: Command line interface for MCP clients with secure execution and customizable security policies
5
+ Project-URL: Homepage, https://github.com/MladenSU/cli-mcp-server
6
+ Project-URL: Documentation, https://github.com/MladenSU/cli-mcp-server#readme
7
+ Project-URL: Repository, https://github.com/MladenSU/cli-mcp-server.git
8
+ Project-URL: Bug Tracker, https://github.com/MladenSU/cli-mcp-server/issues
9
+ Author-email: Mladen <fangs-lever6n@icloud.com>
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: mcp>=1.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # CLI MCP Server
15
+
16
+ ---
17
+
18
+ A secure Model Context Protocol (MCP) server implementation for executing controlled command-line operations with
19
+ comprehensive security
20
+ features.
21
+
22
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
23
+ ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)
24
+ ![MCP Protocol](https://img.shields.io/badge/MCP-Compatible-green)
25
+
26
+ ---
27
+
28
+ # Table of Contents
29
+
30
+ 1. [Overview](#overview)
31
+ 2. [Features](#features)
32
+ 3. [Configuration](#configuration)
33
+ 4. [Available Tools](#available-tools)
34
+ - [run_command](#run_command)
35
+ - [show_security_rules](#show_security_rules)
36
+ 5. [Usage with Claude Desktop](#usage-with-claude-desktop)
37
+ - [Development/Unpublished Servers Configuration](#developmentunpublished-servers-configuration)
38
+ - [Published Servers Configuration](#published-servers-configuration)
39
+ 6. [Security Features](#security-features)
40
+ 7. [Error Handling](#error-handling)
41
+ 8. [Development](#development)
42
+ - [Prerequisites](#prerequisites)
43
+ - [Building and Publishing](#building-and-publishing)
44
+ - [Debugging](#debugging)
45
+ 9. [License](#license)
46
+
47
+ ---
48
+
49
+ ## Overview
50
+
51
+ This MCP server enables secure command-line execution with robust security measures including command whitelisting, path
52
+ validation, and
53
+ execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security.
54
+
55
+ ## Features
56
+
57
+ - 🔒 Secure command execution with strict validation
58
+ - ⚙️ Configurable command and flag whitelisting
59
+ - 🛡️ Path traversal prevention
60
+ - 🚫 Shell operator injection protection
61
+ - ⏱️ Execution timeouts and length limits
62
+ - 📝 Detailed error reporting
63
+ - 🔄 Async operation support
64
+
65
+ ## Configuration
66
+
67
+ Configure the server using environment variables:
68
+
69
+ | Variable | Description | Default |
70
+ |----------------------|------------------------------------------|--------------------|
71
+ | `ALLOWED_DIR` | Base directory for command execution | Required |
72
+ | `ALLOWED_COMMANDS` | Comma-separated list of allowed commands | `ls,cat,pwd` |
73
+ | `ALLOWED_FLAGS` | Comma-separated list of allowed flags | `-l,-a,--help` |
74
+ | `ALLOWED_PATTERNS` | Comma-separated file patterns | `*.txt,*.log,*.md` |
75
+ | `MAX_COMMAND_LENGTH` | Maximum command string length | `1024` |
76
+ | `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
77
+
78
+ ## Available Tools
79
+
80
+ ### run_command
81
+
82
+ Executes whitelisted CLI commands within allowed directories.
83
+
84
+ **Input Schema:**
85
+
86
+ ```json
87
+ {
88
+ "command": {
89
+ "type": "string",
90
+ "description": "Command to execute (e.g., 'ls -l' or 'cat file.txt')"
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### show_security_rules
96
+
97
+ Displays current security configuration and restrictions.
98
+
99
+ ## Usage with Claude Desktop
100
+
101
+ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
102
+
103
+ > Development/Unpublished Servers Configuration
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "cli-mcp-server": {
109
+ "command": "uv",
110
+ "args": [
111
+ "--directory",
112
+ "<path/to/the/repo>/cli-mcp-server",
113
+ "run",
114
+ "cli-mcp-server"
115
+ ],
116
+ "env": {
117
+ "ALLOWED_DIR": "</your/desired/dir>",
118
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo",
119
+ "ALLOWED_FLAGS": "-l,-a,--help,--version",
120
+ "ALLOWED_PATTERNS": "*.txt,*.log,*.md",
121
+ "MAX_COMMAND_LENGTH": "1024",
122
+ "COMMAND_TIMEOUT": "30"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ > Published Servers Configuration
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "cli-mcp-server": {
135
+ "command": "uvx",
136
+ "args": [
137
+ "cli-mcp-server"
138
+ ],
139
+ "env": {
140
+ "ALLOWED_DIR": "</your/desired/dir>",
141
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo",
142
+ "ALLOWED_FLAGS": "-l,-a,--help,--version",
143
+ "ALLOWED_PATTERNS": "*.txt,*.log,*.md",
144
+ "MAX_COMMAND_LENGTH": "1024",
145
+ "COMMAND_TIMEOUT": "30"
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## Security Features
153
+
154
+ - ✅ Command whitelist enforcement
155
+ - ✅ Flag validation
156
+ - ✅ Path traversal prevention
157
+ - ✅ Shell operator blocking
158
+ - ✅ Command length limits
159
+ - ✅ Execution timeouts
160
+ - ✅ Working directory restrictions
161
+
162
+ ## Error Handling
163
+
164
+ The server provides detailed error messages for:
165
+
166
+ - Security violations
167
+ - Command timeouts
168
+ - Invalid command formats
169
+ - Path security violations
170
+ - Execution failures
171
+
172
+ ## Development
173
+
174
+ ### Prerequisites
175
+
176
+ - Python 3.10+
177
+ - MCP protocol library
178
+
179
+ ## Development
180
+
181
+ ### Building and Publishing
182
+
183
+ To prepare the package for distribution:
184
+
185
+ 1. Sync dependencies and update lockfile:
186
+ ```bash
187
+ uv sync
188
+ ```
189
+
190
+ 2. Build package distributions:
191
+ ```bash
192
+ uv build
193
+ ```
194
+
195
+ > This will create source and wheel distributions in the `dist/` directory.
196
+
197
+ 3. Publish to PyPI:
198
+ ```bash
199
+ uv publish --token {{YOUR_PYPI_API_TOKEN}}
200
+ ```
201
+
202
+ ### Debugging
203
+
204
+ Since MCP servers run over stdio, debugging can be challenging. For the best debugging
205
+ experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
206
+
207
+ You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with
208
+ this command:
209
+
210
+ ```bash
211
+ npx @modelcontextprotocol/inspector uv --directory {{your source code local directory}}/unichat-mcp-server run unichat-mcp-server
212
+ ```
213
+
214
+ Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
215
+
216
+ ## License
217
+
218
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
219
+
220
+ ---
221
+
222
+ For more information or support, please open an issue on the project repository.
@@ -0,0 +1,209 @@
1
+ # CLI MCP Server
2
+
3
+ ---
4
+
5
+ A secure Model Context Protocol (MCP) server implementation for executing controlled command-line operations with
6
+ comprehensive security
7
+ features.
8
+
9
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
10
+ ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)
11
+ ![MCP Protocol](https://img.shields.io/badge/MCP-Compatible-green)
12
+
13
+ ---
14
+
15
+ # Table of Contents
16
+
17
+ 1. [Overview](#overview)
18
+ 2. [Features](#features)
19
+ 3. [Configuration](#configuration)
20
+ 4. [Available Tools](#available-tools)
21
+ - [run_command](#run_command)
22
+ - [show_security_rules](#show_security_rules)
23
+ 5. [Usage with Claude Desktop](#usage-with-claude-desktop)
24
+ - [Development/Unpublished Servers Configuration](#developmentunpublished-servers-configuration)
25
+ - [Published Servers Configuration](#published-servers-configuration)
26
+ 6. [Security Features](#security-features)
27
+ 7. [Error Handling](#error-handling)
28
+ 8. [Development](#development)
29
+ - [Prerequisites](#prerequisites)
30
+ - [Building and Publishing](#building-and-publishing)
31
+ - [Debugging](#debugging)
32
+ 9. [License](#license)
33
+
34
+ ---
35
+
36
+ ## Overview
37
+
38
+ This MCP server enables secure command-line execution with robust security measures including command whitelisting, path
39
+ validation, and
40
+ execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security.
41
+
42
+ ## Features
43
+
44
+ - 🔒 Secure command execution with strict validation
45
+ - ⚙️ Configurable command and flag whitelisting
46
+ - 🛡️ Path traversal prevention
47
+ - 🚫 Shell operator injection protection
48
+ - ⏱️ Execution timeouts and length limits
49
+ - 📝 Detailed error reporting
50
+ - 🔄 Async operation support
51
+
52
+ ## Configuration
53
+
54
+ Configure the server using environment variables:
55
+
56
+ | Variable | Description | Default |
57
+ |----------------------|------------------------------------------|--------------------|
58
+ | `ALLOWED_DIR` | Base directory for command execution | Required |
59
+ | `ALLOWED_COMMANDS` | Comma-separated list of allowed commands | `ls,cat,pwd` |
60
+ | `ALLOWED_FLAGS` | Comma-separated list of allowed flags | `-l,-a,--help` |
61
+ | `ALLOWED_PATTERNS` | Comma-separated file patterns | `*.txt,*.log,*.md` |
62
+ | `MAX_COMMAND_LENGTH` | Maximum command string length | `1024` |
63
+ | `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
64
+
65
+ ## Available Tools
66
+
67
+ ### run_command
68
+
69
+ Executes whitelisted CLI commands within allowed directories.
70
+
71
+ **Input Schema:**
72
+
73
+ ```json
74
+ {
75
+ "command": {
76
+ "type": "string",
77
+ "description": "Command to execute (e.g., 'ls -l' or 'cat file.txt')"
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### show_security_rules
83
+
84
+ Displays current security configuration and restrictions.
85
+
86
+ ## Usage with Claude Desktop
87
+
88
+ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
89
+
90
+ > Development/Unpublished Servers Configuration
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "cli-mcp-server": {
96
+ "command": "uv",
97
+ "args": [
98
+ "--directory",
99
+ "<path/to/the/repo>/cli-mcp-server",
100
+ "run",
101
+ "cli-mcp-server"
102
+ ],
103
+ "env": {
104
+ "ALLOWED_DIR": "</your/desired/dir>",
105
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo",
106
+ "ALLOWED_FLAGS": "-l,-a,--help,--version",
107
+ "ALLOWED_PATTERNS": "*.txt,*.log,*.md",
108
+ "MAX_COMMAND_LENGTH": "1024",
109
+ "COMMAND_TIMEOUT": "30"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ > Published Servers Configuration
117
+
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "cli-mcp-server": {
122
+ "command": "uvx",
123
+ "args": [
124
+ "cli-mcp-server"
125
+ ],
126
+ "env": {
127
+ "ALLOWED_DIR": "</your/desired/dir>",
128
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo",
129
+ "ALLOWED_FLAGS": "-l,-a,--help,--version",
130
+ "ALLOWED_PATTERNS": "*.txt,*.log,*.md",
131
+ "MAX_COMMAND_LENGTH": "1024",
132
+ "COMMAND_TIMEOUT": "30"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Security Features
140
+
141
+ - ✅ Command whitelist enforcement
142
+ - ✅ Flag validation
143
+ - ✅ Path traversal prevention
144
+ - ✅ Shell operator blocking
145
+ - ✅ Command length limits
146
+ - ✅ Execution timeouts
147
+ - ✅ Working directory restrictions
148
+
149
+ ## Error Handling
150
+
151
+ The server provides detailed error messages for:
152
+
153
+ - Security violations
154
+ - Command timeouts
155
+ - Invalid command formats
156
+ - Path security violations
157
+ - Execution failures
158
+
159
+ ## Development
160
+
161
+ ### Prerequisites
162
+
163
+ - Python 3.10+
164
+ - MCP protocol library
165
+
166
+ ## Development
167
+
168
+ ### Building and Publishing
169
+
170
+ To prepare the package for distribution:
171
+
172
+ 1. Sync dependencies and update lockfile:
173
+ ```bash
174
+ uv sync
175
+ ```
176
+
177
+ 2. Build package distributions:
178
+ ```bash
179
+ uv build
180
+ ```
181
+
182
+ > This will create source and wheel distributions in the `dist/` directory.
183
+
184
+ 3. Publish to PyPI:
185
+ ```bash
186
+ uv publish --token {{YOUR_PYPI_API_TOKEN}}
187
+ ```
188
+
189
+ ### Debugging
190
+
191
+ Since MCP servers run over stdio, debugging can be challenging. For the best debugging
192
+ experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
193
+
194
+ You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with
195
+ this command:
196
+
197
+ ```bash
198
+ npx @modelcontextprotocol/inspector uv --directory {{your source code local directory}}/unichat-mcp-server run unichat-mcp-server
199
+ ```
200
+
201
+ Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
202
+
203
+ ## License
204
+
205
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
206
+
207
+ ---
208
+
209
+ For more information or support, please open an issue on the project repository.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "cli-mcp-server"
3
+ version = "0.1.2"
4
+ description = "Command line interface for MCP clients with secure execution and customizable security policies"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = ["mcp>=1.0.0"]
8
+ authors = [
9
+ { name = "Mladen", email = "fangs-lever6n@icloud.com" },
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [project.scripts]
17
+ cli-mcp-server = "cli_mcp_server:main"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/MladenSU/cli-mcp-server"
21
+ Documentation = "https://github.com/MladenSU/cli-mcp-server#readme"
22
+ Repository = "https://github.com/MladenSU/cli-mcp-server.git"
23
+ "Bug Tracker" = "https://github.com/MladenSU/cli-mcp-server/issues"
@@ -0,0 +1,12 @@
1
+ import asyncio
2
+
3
+ from . import server
4
+
5
+
6
+ def main():
7
+ """Main entry point for the package."""
8
+ asyncio.run(server.main())
9
+
10
+
11
+ # Optionally expose other important items at package level
12
+ __all__ = ['main', 'server']
@@ -0,0 +1,337 @@
1
+ import os
2
+ import re
3
+ import shlex
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from typing import List, Dict, Any, Optional
7
+
8
+ import mcp.server.stdio
9
+ import mcp.types as types
10
+ from mcp.server import NotificationOptions, Server
11
+ from mcp.server.models import InitializationOptions
12
+
13
+ server = Server("cli-mcp-server")
14
+
15
+
16
+ class CommandSecurityError(Exception):
17
+ """
18
+ Custom exception for command security violations
19
+ """
20
+
21
+ pass
22
+
23
+
24
+ @dataclass
25
+ class SecurityConfig:
26
+ """
27
+ Security configuration for command execution
28
+ """
29
+
30
+ allowed_commands: set[str]
31
+ allowed_flags: set[str]
32
+ allowed_patterns: List[str]
33
+ max_command_length: int
34
+ command_timeout: int
35
+
36
+
37
+ class CommandExecutor:
38
+ def __init__(self, allowed_dir: str, security_config: SecurityConfig):
39
+ if not allowed_dir or not os.path.exists(allowed_dir):
40
+ raise ValueError("Valid ALLOWED_DIR is required")
41
+ self.allowed_dir = os.path.abspath(os.path.realpath(allowed_dir))
42
+ self.security_config = security_config
43
+
44
+ def validate_command(self, command_string: str) -> tuple[str, List[str]]:
45
+ """
46
+ Validates and parses a command string for security and formatting.
47
+
48
+ Checks the command string for unsupported shell operators and splits it into
49
+ command and arguments. Only single commands without shell operators are allowed.
50
+
51
+ Args:
52
+ command_string (str): The command string to validate and parse.
53
+
54
+ Returns:
55
+ tuple[str, List[str]]: A tuple containing:
56
+ - The command name (str)
57
+ - List of command arguments (List[str])
58
+
59
+ Raises:
60
+ CommandSecurityError: If the command contains unsupported shell operators.
61
+ """
62
+
63
+ # Check for shell operators that we don't support
64
+ shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
65
+ for operator in shell_operators:
66
+ if operator in command_string:
67
+ raise CommandSecurityError(
68
+ f"Shell operator '{operator}' is not supported. "
69
+ "Only single commands are allowed."
70
+ )
71
+
72
+ try:
73
+ parts = shlex.split(command_string)
74
+ if not parts:
75
+ raise CommandSecurityError("Empty command")
76
+
77
+ command, args = parts[0], parts[1:]
78
+
79
+ # Validate command
80
+ if command not in self.security_config.allowed_commands:
81
+ raise CommandSecurityError(f"Command '{command}' is not allowed")
82
+
83
+ # Validate arguments
84
+ for arg in args:
85
+ if arg.startswith("-"):
86
+ if arg not in self.security_config.allowed_flags:
87
+ raise CommandSecurityError(f"Flag '{arg}' is not allowed")
88
+ continue
89
+
90
+ # Validate path if argument looks like a path
91
+ if "/" in arg or "\\" in arg or os.path.isabs(arg):
92
+ full_path = os.path.abspath(os.path.join(self.allowed_dir, arg))
93
+ if not self._is_path_safe(full_path):
94
+ raise CommandSecurityError(f"Path '{arg}' is not allowed")
95
+
96
+ # Check patterns
97
+ if not any(
98
+ re.match(pattern, arg)
99
+ for pattern in self.security_config.allowed_patterns
100
+ ):
101
+ raise CommandSecurityError(
102
+ f"Argument '{arg}' doesn't match allowed patterns"
103
+ )
104
+
105
+ return command, args
106
+ except ValueError as e:
107
+ raise CommandSecurityError(f"Invalid command format: {str(e)}")
108
+
109
+ def _is_path_safe(self, path: str) -> bool:
110
+ """
111
+ Checks if a given path is safe to access within allowed directory boundaries.
112
+
113
+ Validates that the absolute resolved path is within the allowed directory
114
+ to prevent directory traversal attacks.
115
+
116
+ Args:
117
+ path (str): The path to validate.
118
+
119
+ Returns:
120
+ bool: True if path is within allowed directory, False otherwise.
121
+ Returns False if path resolution fails for any reason.
122
+
123
+ Private method intended for internal use only.
124
+ """
125
+ try:
126
+ abs_path = os.path.abspath(os.path.realpath(path))
127
+ return abs_path.startswith(self.allowed_dir)
128
+ except Exception:
129
+ return False
130
+
131
+ def execute(self, command_string: str) -> subprocess.CompletedProcess:
132
+ """
133
+ Executes a command string in a secure, controlled environment.
134
+
135
+ Runs the command after validating it against security constraints including length limits
136
+ and shell operator restrictions. Executes with controlled parameters for safety.
137
+
138
+ Args:
139
+ command_string (str): The command string to execute.
140
+
141
+ Returns:
142
+ subprocess.CompletedProcess: The result of the command execution containing
143
+ stdout, stderr, and return code.
144
+
145
+ Raises:
146
+ CommandSecurityError: If the command:
147
+ - Exceeds maximum length
148
+ - Contains invalid shell operators
149
+ - Fails security validation
150
+ - Fails during execution
151
+
152
+ Notes:
153
+ - Executes with shell=False for security
154
+ - Uses timeout and working directory constraints
155
+ - Captures both stdout and stderr
156
+ """
157
+ if len(command_string) > self.security_config.max_command_length:
158
+ raise CommandSecurityError("Command string too long")
159
+
160
+ try:
161
+
162
+ command, args = self.validate_command(command_string)
163
+ return subprocess.run(
164
+ [command] + args,
165
+ shell=False,
166
+ text=True,
167
+ capture_output=True,
168
+ timeout=self.security_config.command_timeout,
169
+ cwd=self.allowed_dir,
170
+ )
171
+ except Exception as e:
172
+ if isinstance(e, CommandSecurityError):
173
+ raise
174
+ raise CommandSecurityError(f"Command execution failed: {str(e)}")
175
+
176
+
177
+ # Load security configuration from environment
178
+ def load_security_config() -> SecurityConfig:
179
+ """
180
+ Loads security configuration from environment variables with default fallbacks.
181
+
182
+ Creates a SecurityConfig instance using environment variables to configure allowed
183
+ commands, flags, patterns, and execution constraints. Uses predefined defaults if
184
+ environment variables are not set.
185
+
186
+ Returns:
187
+ SecurityConfig: Configuration object containing:
188
+ - allowed_commands: Set of permitted command names
189
+ - allowed_flags: Set of permitted command flags/options
190
+ - allowed_patterns: List of regex patterns for valid inputs
191
+ - max_command_length: Maximum length of command string
192
+ - command_timeout: Maximum execution time in seconds
193
+
194
+ Environment Variables:
195
+ ALLOWED_COMMANDS: Comma-separated list of allowed commands (default: "ls,cat,pwd")
196
+ ALLOWED_FLAGS: Comma-separated list of allowed flags (default: "-l,-a,--help")
197
+ ALLOWED_PATTERNS: Comma-separated list of patterns (default: "*.txt,*.log,*.md")
198
+ MAX_COMMAND_LENGTH: Maximum command string length (default: 1024)
199
+ COMMAND_TIMEOUT: Command timeout in seconds (default: 30)
200
+ """
201
+ return SecurityConfig(
202
+ allowed_commands=set(os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd").split(",")),
203
+ allowed_flags=set(os.getenv("ALLOWED_FLAGS", "-l,-a,--help").split(",")),
204
+ allowed_patterns=[
205
+ r"^[\w\-. ]+$", # Basic filename pattern
206
+ *os.getenv("ALLOWED_PATTERNS", "*.txt,*.log,*.md").split(","),
207
+ ],
208
+ max_command_length=int(os.getenv("MAX_COMMAND_LENGTH", "1024")),
209
+ command_timeout=int(os.getenv("COMMAND_TIMEOUT", "30")),
210
+ )
211
+
212
+
213
+ executor = CommandExecutor(
214
+ allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config()
215
+ )
216
+
217
+
218
+ @server.list_tools()
219
+ async def handle_list_tools() -> list[types.Tool]:
220
+ return [
221
+ types.Tool(
222
+ name="run_command",
223
+ description=(
224
+ f"Allows command (CLI) execution in the directory: {executor.allowed_dir}\n\n"
225
+ f"Available commands: {', '.join(executor.security_config.allowed_commands)}\n"
226
+ f"Available flags: {', '.join(executor.security_config.allowed_flags)}\n\n"
227
+ "Note: Shell operators (&&, |, >, >>) are not supported."
228
+ ),
229
+ inputSchema={
230
+ "type": "object",
231
+ "properties": {
232
+ "command": {
233
+ "type": "string",
234
+ "description": "Single command to execute (example: 'ls -l' or 'cat file.txt')"
235
+ }
236
+ },
237
+ "required": ["command"],
238
+ },
239
+ ),
240
+ types.Tool(
241
+ name="show_security_rules",
242
+ description=(
243
+ "Show what commands and operations are allowed in this environment.\n"
244
+ ),
245
+ inputSchema={
246
+ "type": "object",
247
+ "properties": {},
248
+ },
249
+ )
250
+ ]
251
+
252
+
253
+ @server.call_tool()
254
+ async def handle_call_tool(
255
+ name: str, arguments: Optional[Dict[str, Any]]
256
+ ) -> List[types.TextContent]:
257
+ if name == "run_command":
258
+ if not arguments or "command" not in arguments:
259
+ return [
260
+ types.TextContent(type="text", text="No command provided", error=True)
261
+ ]
262
+
263
+ try:
264
+ result = executor.execute(arguments["command"])
265
+
266
+ response = []
267
+ if result.stdout:
268
+ response.append(types.TextContent(type="text", text=result.stdout))
269
+ if result.stderr:
270
+ response.append(
271
+ types.TextContent(type="text", text=result.stderr, error=True)
272
+ )
273
+
274
+ response.append(
275
+ types.TextContent(
276
+ type="text",
277
+ text=f"\nCommand completed with return code: {result.returncode}",
278
+ )
279
+ )
280
+
281
+ return response
282
+
283
+ except CommandSecurityError as e:
284
+ return [
285
+ types.TextContent(
286
+ type="text", text=f"Security violation: {str(e)}", error=True
287
+ )
288
+ ]
289
+ except subprocess.TimeoutExpired:
290
+ return [
291
+ types.TextContent(
292
+ type="text",
293
+ text=f"Command timed out after {executor.security_config.command_timeout} seconds",
294
+ error=True,
295
+ )
296
+ ]
297
+ except Exception as e:
298
+ return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)]
299
+
300
+ elif name == "show_security_rules":
301
+ security_info = (
302
+ "Security Configuration:\n"
303
+ f"==================\n"
304
+ f"Working Directory: {executor.allowed_dir}\n"
305
+ f"\nAllowed Commands:\n"
306
+ f"----------------\n"
307
+ f"{', '.join(sorted(executor.security_config.allowed_commands))}\n"
308
+ f"\nAllowed Flags:\n"
309
+ f"-------------\n"
310
+ f"{', '.join(sorted(executor.security_config.allowed_flags))}\n"
311
+ f"\nAllowed Patterns:\n"
312
+ f"----------------\n"
313
+ f"{', '.join(executor.security_config.allowed_patterns)}\n"
314
+ f"\nSecurity Limits:\n"
315
+ f"---------------\n"
316
+ f"Max Command Length: {executor.security_config.max_command_length} characters\n"
317
+ f"Command Timeout: {executor.security_config.command_timeout} seconds\n"
318
+ )
319
+ return [types.TextContent(type="text", text=security_info)]
320
+
321
+ raise ValueError(f"Unknown tool: {name}")
322
+
323
+
324
+ async def main():
325
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
326
+ await server.run(
327
+ read_stream,
328
+ write_stream,
329
+ InitializationOptions(
330
+ server_name="cli-mcp-server",
331
+ server_version="0.1.1",
332
+ capabilities=server.get_capabilities(
333
+ notification_options=NotificationOptions(),
334
+ experimental_capabilities={},
335
+ ),
336
+ ),
337
+ )
@@ -0,0 +1,295 @@
1
+ version = 1
2
+ requires-python = ">=3.10"
3
+
4
+ [[package]]
5
+ name = "annotated-types"
6
+ version = "0.7.0"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
9
+ wheels = [
10
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "anyio"
15
+ version = "4.6.2.post1"
16
+ source = { registry = "https://pypi.org/simple" }
17
+ dependencies = [
18
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
19
+ { name = "idna" },
20
+ { name = "sniffio" },
21
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "certifi"
30
+ version = "2024.8.30"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
35
+ ]
36
+
37
+ [[package]]
38
+ name = "cli-mcp-server"
39
+ version = "0.1.2"
40
+ source = { editable = "." }
41
+ dependencies = [
42
+ { name = "mcp" },
43
+ ]
44
+
45
+ [package.metadata]
46
+ requires-dist = [{ name = "mcp", specifier = ">=1.0.0" }]
47
+
48
+ [[package]]
49
+ name = "click"
50
+ version = "8.1.7"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ dependencies = [
53
+ { name = "colorama", marker = "platform_system == 'Windows'" },
54
+ ]
55
+ sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "colorama"
62
+ version = "0.4.6"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "exceptiongroup"
71
+ version = "1.2.2"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "h11"
80
+ version = "0.14.0"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "httpcore"
89
+ version = "1.0.7"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ dependencies = [
92
+ { name = "certifi" },
93
+ { name = "h11" },
94
+ ]
95
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
96
+ wheels = [
97
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
98
+ ]
99
+
100
+ [[package]]
101
+ name = "httpx"
102
+ version = "0.28.0"
103
+ source = { registry = "https://pypi.org/simple" }
104
+ dependencies = [
105
+ { name = "anyio" },
106
+ { name = "certifi" },
107
+ { name = "httpcore" },
108
+ { name = "idna" },
109
+ ]
110
+ sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "httpx-sse"
117
+ version = "0.4.0"
118
+ source = { registry = "https://pypi.org/simple" }
119
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
120
+ wheels = [
121
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "idna"
126
+ version = "3.10"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
131
+ ]
132
+
133
+ [[package]]
134
+ name = "mcp"
135
+ version = "1.0.0"
136
+ source = { registry = "https://pypi.org/simple" }
137
+ dependencies = [
138
+ { name = "anyio" },
139
+ { name = "httpx" },
140
+ { name = "httpx-sse" },
141
+ { name = "pydantic" },
142
+ { name = "sse-starlette" },
143
+ { name = "starlette" },
144
+ ]
145
+ sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
146
+ wheels = [
147
+ { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
148
+ ]
149
+
150
+ [[package]]
151
+ name = "pydantic"
152
+ version = "2.10.3"
153
+ source = { registry = "https://pypi.org/simple" }
154
+ dependencies = [
155
+ { name = "annotated-types" },
156
+ { name = "pydantic-core" },
157
+ { name = "typing-extensions" },
158
+ ]
159
+ sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 }
160
+ wheels = [
161
+ { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 },
162
+ ]
163
+
164
+ [[package]]
165
+ name = "pydantic-core"
166
+ version = "2.27.1"
167
+ source = { registry = "https://pypi.org/simple" }
168
+ dependencies = [
169
+ { name = "typing-extensions" },
170
+ ]
171
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 },
174
+ { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 },
175
+ { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 },
176
+ { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 },
177
+ { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 },
178
+ { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 },
179
+ { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 },
180
+ { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 },
181
+ { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 },
182
+ { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 },
183
+ { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 },
184
+ { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 },
185
+ { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 },
186
+ { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 },
187
+ { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 },
188
+ { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 },
189
+ { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 },
190
+ { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 },
191
+ { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 },
192
+ { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 },
193
+ { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 },
194
+ { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 },
195
+ { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 },
196
+ { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 },
197
+ { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 },
198
+ { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 },
199
+ { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 },
200
+ { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
201
+ { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
202
+ { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
203
+ { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
204
+ { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
205
+ { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
206
+ { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
207
+ { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
208
+ { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
209
+ { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
210
+ { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
211
+ { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
212
+ { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
213
+ { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
214
+ { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
215
+ { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
216
+ { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
217
+ { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 },
218
+ { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 },
219
+ { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 },
220
+ { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 },
221
+ { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 },
222
+ { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 },
223
+ { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 },
224
+ { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 },
225
+ { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 },
226
+ { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 },
227
+ { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
228
+ { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 },
229
+ { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 },
230
+ { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 },
231
+ { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 },
232
+ { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 },
233
+ { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 },
234
+ { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 },
235
+ { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 },
236
+ { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 },
237
+ ]
238
+
239
+ [[package]]
240
+ name = "sniffio"
241
+ version = "1.3.1"
242
+ source = { registry = "https://pypi.org/simple" }
243
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
244
+ wheels = [
245
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
246
+ ]
247
+
248
+ [[package]]
249
+ name = "sse-starlette"
250
+ version = "2.1.3"
251
+ source = { registry = "https://pypi.org/simple" }
252
+ dependencies = [
253
+ { name = "anyio" },
254
+ { name = "starlette" },
255
+ { name = "uvicorn" },
256
+ ]
257
+ sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 }
258
+ wheels = [
259
+ { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 },
260
+ ]
261
+
262
+ [[package]]
263
+ name = "starlette"
264
+ version = "0.41.3"
265
+ source = { registry = "https://pypi.org/simple" }
266
+ dependencies = [
267
+ { name = "anyio" },
268
+ ]
269
+ sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
270
+ wheels = [
271
+ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
272
+ ]
273
+
274
+ [[package]]
275
+ name = "typing-extensions"
276
+ version = "4.12.2"
277
+ source = { registry = "https://pypi.org/simple" }
278
+ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
279
+ wheels = [
280
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
281
+ ]
282
+
283
+ [[package]]
284
+ name = "uvicorn"
285
+ version = "0.32.1"
286
+ source = { registry = "https://pypi.org/simple" }
287
+ dependencies = [
288
+ { name = "click" },
289
+ { name = "h11" },
290
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
291
+ ]
292
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 }
293
+ wheels = [
294
+ { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 },
295
+ ]