diff --git a/src/aws/.python-version b/src/aws/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/src/aws/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/src/aws/README.md b/src/aws/README.md new file mode 100644 index 00000000..ebb325c3 --- /dev/null +++ b/src/aws/README.md @@ -0,0 +1,125 @@ +# AWS MCP Server + +An MCP server implementation for AWS operations, supporting S3 and DynamoDB services. All operations are automatically logged and can be accessed through the `audit://aws-operations` resource endpoint. + +## Setup + +1. Install dependencies +2. Configure AWS credentials using either: + - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`) + - AWS credentials file +3. Run the server + +## Available Tools + +### S3 Operations + +- **s3_bucket_create**: Create a new S3 bucket + - Required: `bucket_name` + +- **s3_bucket_list**: List all S3 buckets + +- **s3_bucket_delete**: Delete an S3 bucket + - Required: `bucket_name` + +- **s3_object_upload**: Upload an object to S3 + - Required: `bucket_name`, `object_key`, `file_content` (Base64 encoded) + +- **s3_object_delete**: Delete an object from S3 + - Required: `bucket_name`, `object_key` + +- **s3_object_list**: List objects in an S3 bucket + - Required: `bucket_name` + +- **s3_object_read**: Read an object's content from S3 + - Required: `bucket_name`, `object_key` + +### DynamoDB Operations + +#### Table Operations +- **dynamodb_table_create**: Create a new DynamoDB table + - Required: `table_name`, `key_schema`, `attribute_definitions` + +- **dynamodb_table_describe**: Get details about a DynamoDB table + - Required: `table_name` + +- **dynamodb_table_list**: List all DynamoDB tables + +- **dynamodb_table_delete**: Delete a DynamoDB table + - Required: `table_name` + +- **dynamodb_table_update**: Update a DynamoDB table + - Required: `table_name`, `attribute_definitions` + +#### Item Operations +- **dynamodb_item_put**: Put an item into a DynamoDB table + - Required: `table_name`, `item` + +- **dynamodb_item_get**: Get an item from a DynamoDB table + - Required: `table_name`, `key` + +- **dynamodb_item_update**: Update an item in a DynamoDB table + - Required: `table_name`, `key`, `item` + +- **dynamodb_item_delete**: Delete an item from a DynamoDB table + - Required: `table_name`, `key` + +- **dynamodb_item_query**: Query items in a DynamoDB table + - Required: `table_name`, `key_condition`, `expression_values` + +- **dynamodb_item_scan**: Scan items in a DynamoDB table + - Required: `table_name` + - Optional: `filter_expression`, `expression_attributes` (with `values` and `names`) + +#### Batch Operations +- **dynamodb_batch_get**: Batch get multiple items from DynamoDB tables + - Required: `request_items` + +- **dynamodb_item_batch_write**: Batch write operations (put/delete) for DynamoDB items + - Required: `table_name`, `operation` (put/delete), `items` + - Optional: `key_attributes` (for delete operations) + +- **dynamodb_batch_execute**: Execute multiple PartiQL statements in a batch + - Required: `statements`, `parameters` + +#### TTL Operations +- **dynamodb_describe_ttl**: Get the TTL settings for a table + - Required: `table_name` + +- **dynamodb_update_ttl**: Update the TTL settings for a table + - Required: `table_name`, `ttl_enabled`, `ttl_attribute` + +## Configuration + +The server requires AWS credentials to be configured in one of these ways: +- Environment variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY +- Default AWS credential chain (e.g., AWS CLI configuration) + +Additional configuration: +- AWS_REGION (defaults to "us-east-1") + +## Quickstart + +### Install + +#### Claude Desktop + +On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` +On Windows: `%APPDATA%/Claude/claude_desktop_config.json` + +
+ Development/Unpublished Servers Configuration + ``` + "mcpServers": { + "mcp-server-aws": { + "command": "uv", + "args": [ + "--directory", + "/path/to/repo/servers/src/aws", + "run", + "mcp-server-aws" + ] + } + } + ``` +
diff --git a/src/aws/pyproject.toml b/src/aws/pyproject.toml new file mode 100644 index 00000000..3d5b4a4a --- /dev/null +++ b/src/aws/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "mcp-server-aws" +version = "0.1.0" +description = "A Model Context Protocol server providing tools to read and manipulate AWS resources using an LLM" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp>=1.0.0", + "python-dotenv>=1.0.1", + "boto3>=1.35.53", +] +[[project.authors]] +name = "Rishi Kavikondala" +email = "rishi.kavikondala@gmail.com" + +[build-system] +requires = [ "hatchling",] +build-backend = "hatchling.build" + +[project.scripts] +mcp-server-aws = "mcp_server_aws:main" diff --git a/src/aws/src/mcp_server_aws/__init__.py b/src/aws/src/mcp_server_aws/__init__.py new file mode 100644 index 00000000..6217b1fe --- /dev/null +++ b/src/aws/src/mcp_server_aws/__init__.py @@ -0,0 +1,9 @@ +from . import server +import asyncio + +def main(): + """Main entry point for the package.""" + asyncio.run(server.main()) + +# Optionally expose other important items at package level +__all__ = ['main', 'server'] \ No newline at end of file diff --git a/src/aws/src/mcp_server_aws/server.py b/src/aws/src/mcp_server_aws/server.py new file mode 100644 index 00000000..5e1f3fd9 --- /dev/null +++ b/src/aws/src/mcp_server_aws/server.py @@ -0,0 +1,377 @@ +import os +import json +import logging +from datetime import datetime +from pathlib import Path +from typing import Any, Sequence +from functools import lru_cache +import base64 +import io +import boto3 +import asyncio +from dotenv import load_dotenv +import mcp.server.stdio +from mcp.server import Server, NotificationOptions +from mcp.server.models import InitializationOptions +from mcp.types import Resource, Tool, TextContent, ImageContent, EmbeddedResource +from pydantic import AnyUrl +from .tools import get_aws_tools +from .utils import get_dynamodb_type + +load_dotenv() +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("aws-mcp-server") + + +def custom_json_serializer(obj): + if isinstance(obj, datetime): + return obj.isoformat() + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + +class AWSManager: + def __init__(self): + logger.info("Initializing AWSManager") + self.audit_entries: list[dict] = [] + + @lru_cache(maxsize=None) + def get_boto3_client(self, service_name: str, region_name: str = None): + """Get a boto3 client using explicit credentials if available""" + try: + logger.info(f"Creating boto3 client for service: {service_name}") + region_name = region_name or os.getenv("AWS_REGION", "us-east-1") + if not region_name: + raise ValueError( + "AWS region is not specified and not set in the environment.") + + aws_access_key = os.getenv("AWS_ACCESS_KEY_ID") + aws_secret_key = os.getenv("AWS_SECRET_ACCESS_KEY") + + if aws_access_key and aws_secret_key: + logger.debug("Using explicit AWS credentials") + session = boto3.Session( + aws_access_key_id=aws_access_key, + aws_secret_access_key=aws_secret_key, + region_name=region_name + ) + else: + logger.debug("Using default AWS credential chain") + session = boto3.Session(region_name=region_name) + + return session.client(service_name) + except Exception as e: + logger.error(f"Failed to create boto3 client for { + service_name}: {e}") + raise RuntimeError(f"Failed to create boto3 client: {e}") + + def _synthesize_audit_log(self) -> str: + """Generate formatted audit log from entries""" + logger.debug("Synthesizing audit log") + if not self.audit_entries: + return "No AWS operations have been performed yet." + + report = "📋 AWS Operations Audit Log 📋\n\n" + for entry in self.audit_entries: + report += f"[{entry['timestamp']}]\n" + report += f"Service: {entry['service']}\n" + report += f"Operation: {entry['operation']}\n" + report += f"Parameters: {json.dumps( + entry['parameters'], indent=2)}\n" + report += "-" * 50 + "\n" + + return report + + def log_operation(self, service: str, operation: str, parameters: dict) -> None: + """Log an AWS operation to the audit log""" + logger.info( + f"Logging operation - Service: {service}, Operation: {operation}") + audit_entry = { + "timestamp": datetime.utcnow().isoformat(), + "service": service, + "operation": operation, + "parameters": parameters + } + self.audit_entries.append(audit_entry) + + +async def main(): + logger.info("Starting AWS MCP Server") + + aws = AWSManager() + server = Server("aws-mcp-server") + + logger.debug("Registering handlers") + + @server.list_resources() + async def handle_list_resources() -> list[Resource]: + logger.debug("Handling list_resources request") + return [ + Resource( + uri=AnyUrl("audit://aws-operations"), + name="AWS Operations Audit Log", + description="A log of all AWS operations performed through this server", + mimeType="text/plain", + ) + ] + + @server.read_resource() + async def handle_read_resource(uri: AnyUrl) -> str: + logger.debug(f"Handling read_resource request for URI: {uri}") + if uri.scheme != "audit": + logger.error(f"Unsupported URI scheme: {uri.scheme}") + raise ValueError(f"Unsupported URI scheme: {uri.scheme}") + + path = str(uri).replace("audit://", "") + if path != "aws-operations": + logger.error(f"Unknown resource path: {path}") + raise ValueError(f"Unknown resource path: {path}") + + return aws._synthesize_audit_log() + + @server.list_tools() + async def list_tools() -> list[Tool]: + """List available AWS tools""" + logger.debug("Handling list_tools request") + return get_aws_tools() + + async def handle_s3_operations(aws: AWSManager, name: str, arguments: dict) -> list[TextContent]: + """Handle S3-specific operations""" + s3_client = aws.get_boto3_client('s3') + response = None + + if name == "s3_bucket_create": + response = s3_client.create_bucket(Bucket=arguments["bucket_name"]) + elif name == "s3_bucket_list": + response = s3_client.list_buckets() + elif name == "s3_bucket_delete": + response = s3_client.delete_bucket(Bucket=arguments["bucket_name"]) + elif name == "s3_object_upload": + response = s3_client.upload_fileobj( + io.BytesIO(base64.b64decode(arguments["file_content"])), + arguments["bucket_name"], + arguments["object_key"]) + elif name == "s3_object_delete": + response = s3_client.delete_object( + Bucket=arguments["bucket_name"], + Key=arguments["object_key"] + ) + elif name == "s3_object_list": + response = s3_client.list_objects_v2( + Bucket=arguments["bucket_name"]) + elif name == "s3_object_read": + logging.info(f"Reading object: {arguments['object_key']}") + response = s3_client.get_object( + Bucket=arguments["bucket_name"], + Key=arguments["object_key"] + ) + content = response['Body'].read().decode('utf-8') + return [TextContent(type="text", text=content)] + else: + raise ValueError(f"Unknown S3 operation: {name}") + + aws.log_operation("s3", name.replace("s3_", ""), arguments) + return [TextContent(type="text", text=f"Operation Result:\n{json.dumps(response, indent=2, default=custom_json_serializer)}")] + + async def handle_dynamodb_operations(aws: AWSManager, name: str, arguments: dict) -> list[TextContent]: + """Handle DynamoDB-specific operations""" + dynamodb_client = aws.get_boto3_client('dynamodb') + response = None + + if name == "dynamodb_table_create": + response = dynamodb_client.create_table( + TableName=arguments["table_name"], + KeySchema=arguments["key_schema"], + AttributeDefinitions=arguments["attribute_definitions"], + BillingMode="PAY_PER_REQUEST" + ) + elif name == "dynamodb_table_describe": + response = dynamodb_client.describe_table( + TableName=arguments["table_name"]) + elif name == "dynamodb_table_list": + response = dynamodb_client.list_tables() + elif name == "dynamodb_table_delete": + response = dynamodb_client.delete_table( + TableName=arguments["table_name"]) + elif name == "dynamodb_table_update": + update_params = { + "TableName": arguments["table_name"], + "AttributeDefinitions": arguments["attribute_definitions"] + } + response = dynamodb_client.update_table(**update_params) + elif name == "dynamodb_describe_ttl": + response = dynamodb_client.describe_time_to_live( + TableName=arguments["table_name"] + ) + elif name == "dynamodb_update_ttl": + response = dynamodb_client.update_time_to_live( + TableName=arguments["table_name"], + TimeToLiveSpecification={ + 'Enabled': arguments["ttl_enabled"], + 'AttributeName': arguments["ttl_attribute"] + } + ) + elif name == "dynamodb_item_put": + response = dynamodb_client.put_item( + TableName=arguments["table_name"], + Item=arguments["item"] + ) + elif name == "dynamodb_item_get": + response = dynamodb_client.get_item( + TableName=arguments["table_name"], + Key=arguments["key"] + ) + elif name == "dynamodb_item_update": + response = dynamodb_client.update_item( + TableName=arguments["table_name"], + Key=arguments["key"], + AttributeUpdates=arguments["item"] + ) + elif name == "dynamodb_item_delete": + response = dynamodb_client.delete_item( + TableName=arguments["table_name"], + Key=arguments["key"] + ) + elif name == "dynamodb_item_query": + response = dynamodb_client.query( + TableName=arguments["table_name"], + KeyConditionExpression=arguments["key_condition"], + ExpressionAttributeValues=arguments["expression_values"] + ) + elif name == "dynamodb_item_scan": + scan_params = {"TableName": arguments["table_name"]} + + if "filter_expression" in arguments: + scan_params["FilterExpression"] = arguments["filter_expression"] + + if "expression_attributes" in arguments: + attrs = arguments["expression_attributes"] + if "names" in attrs: + scan_params["ExpressionAttributeNames"] = attrs["names"] + if "values" in attrs: + scan_params["ExpressionAttributeValues"] = attrs["values"] + + response = dynamodb_client.scan(**scan_params) + elif name == "dynamodb_batch_get": + response = dynamodb_client.batch_get_item( + RequestItems=arguments["request_items"] + ) + elif name == "dynamodb_item_batch_write": + table_name = arguments["table_name"] + operation = arguments["operation"] + items = arguments["items"] + + if not items: + raise ValueError("No items provided for batch operation") + + batch_size = 25 + total_items = len(items) + processed_items = 0 + failed_items = [] + + for i in range(0, total_items, batch_size): + batch = items[i:i + batch_size] + request_items = {table_name: []} + + for item in batch: + if operation == "put": + formatted_item = {k: get_dynamodb_type( + v) for k, v in item.items()} + request_items[table_name].append({ + 'PutRequest': {'Item': formatted_item} + }) + elif operation == "delete": + key_attrs = arguments.get( + "key_attributes", list(item.keys())) + formatted_key = {k: get_dynamodb_type( + item[k]) for k in key_attrs} + request_items[table_name].append({ + 'DeleteRequest': {'Key': formatted_key} + }) + + try: + response = dynamodb_client.batch_write_item( + RequestItems=request_items) + processed_items += len(batch) - len( + response.get('UnprocessedItems', {} + ).get(table_name, []) + ) + + unprocessed = response.get('UnprocessedItems', {}) + retry_count = 0 + max_retries = 3 + while unprocessed and retry_count < max_retries: + await asyncio.sleep(2 ** retry_count) + retry_response = dynamodb_client.batch_write_item( + RequestItems=unprocessed) + unprocessed = retry_response.get( + 'UnprocessedItems', {}) + retry_count += 1 + + if unprocessed: + failed_items.extend([ + item['PutRequest']['Item'] if 'PutRequest' in item else item['DeleteRequest']['Key'] + for item in unprocessed.get(table_name, []) + ]) + + except Exception as e: + logger.error(f"Error processing batch: {str(e)}") + failed_items.extend(batch) + + response = { + "total_items": total_items, + "processed_items": processed_items, + "failed_items": len(failed_items), + "failed_items_details": failed_items if failed_items else None + } + elif name == "dynamodb_batch_execute": + response = dynamodb_client.batch_execute_statement( + Statements=[{ + 'Statement': statement, + 'Parameters': params + } for statement, params in zip(arguments["statements"], arguments["parameters"])] + ) + else: + raise ValueError(f"Unknown DynamoDB operation: {name}") + + aws.log_operation("dynamodb", name.replace("dynamodb_", ""), arguments) + return [TextContent(type="text", text=f"Operation Result:\n{json.dumps(response, indent=2, default=custom_json_serializer)}")] + + @server.call_tool() + async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + """Handle AWS tool operations""" + logger.info(f"Handling tool call: {name}") + logger.debug(f"Tool arguments: {arguments}") + + if not isinstance(arguments, dict): + logger.error("Invalid arguments: not a dictionary") + raise ValueError("Invalid arguments") + + try: + if name.startswith("s3_"): + return await handle_s3_operations(aws, name, arguments) + elif name.startswith("dynamodb_"): + return await handle_dynamodb_operations(aws, name, arguments) + else: + raise ValueError(f"Unknown tool: {name}") + + except Exception as e: + logger.error(f"Operation failed: {str(e)}") + raise RuntimeError(f"Operation failed: {str(e)}") + + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + logger.info("Server running with stdio transport") + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="aws", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/aws/src/mcp_server_aws/tools.py b/src/aws/src/mcp_server_aws/tools.py new file mode 100644 index 00000000..3cc21ecf --- /dev/null +++ b/src/aws/src/mcp_server_aws/tools.py @@ -0,0 +1,454 @@ +from mcp.types import Tool + + +def get_s3_tools() -> list[Tool]: + return [ + Tool( + name="s3_bucket_create", + description="Create a new S3 bucket", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket to create" + } + }, + "required": ["bucket_name"] + } + ), + Tool( + name="s3_bucket_list", + description="List all S3 buckets", + inputSchema={ + "type": "object", + "properties": {} + } + ), + Tool( + name="s3_bucket_delete", + description="Delete an S3 bucket", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket to delete" + } + }, + "required": ["bucket_name"] + } + ), + Tool( + name="s3_object_upload", + description="Upload an object to S3", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket" + }, + "object_key": { + "type": "string", + "description": "Key/path of the object in the bucket" + }, + "file_content": { + "type": "string", + "description": "Base64 encoded file content for upload" + } + }, + "required": ["bucket_name", "object_key", "file_content"] + } + ), + Tool( + name="s3_object_delete", + description="Delete an object from S3", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket" + }, + "object_key": { + "type": "string", + "description": "Key/path of the object to delete" + } + }, + "required": ["bucket_name", "object_key"] + } + ), + Tool( + name="s3_object_list", + description="List objects in an S3 bucket", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket" + } + }, + "required": ["bucket_name"] + } + ), + Tool( + name="s3_object_read", + description="Read an object's content from S3", + inputSchema={ + "type": "object", + "properties": { + "bucket_name": { + "type": "string", + "description": "Name of the S3 bucket" + }, + "object_key": { + "type": "string", + "description": "Key/path of the object to read" + } + }, + "required": ["bucket_name", "object_key"] + } + ), + ] + + +def get_dynamodb_tools() -> list[Tool]: + return [ + Tool( + name="dynamodb_table_create", + description="Create a new DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "key_schema": { + "type": "array", + "description": "Key schema for table creation" + }, + "attribute_definitions": { + "type": "array", + "description": "Attribute definitions for table creation" + } + }, + "required": ["table_name", "key_schema", "attribute_definitions"] + } + ), + Tool( + name="dynamodb_table_describe", + description="Get details about a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + } + }, + "required": ["table_name"] + } + ), + Tool( + name="dynamodb_table_list", + description="List all DynamoDB tables", + inputSchema={ + "type": "object", + "properties": {} + } + ), + Tool( + name="dynamodb_table_delete", + description="Delete a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + } + }, + "required": ["table_name"] + } + ), + Tool( + name="dynamodb_table_update", + description="Update a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "attribute_definitions": { + "type": "array", + "description": "Updated attribute definitions" + } + }, + "required": ["table_name", "attribute_definitions"] + } + ), + Tool( + name="dynamodb_item_put", + description="Put an item into a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "item": { + "type": "object", + "description": "Item data to put" + } + }, + "required": ["table_name", "item"] + } + ), + Tool( + name="dynamodb_item_get", + description="Get an item from a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "key": { + "type": "object", + "description": "Key to identify the item" + } + }, + "required": ["table_name", "key"] + } + ), + Tool( + name="dynamodb_item_update", + description="Update an item in a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "key": { + "type": "object", + "description": "Key to identify the item" + }, + "item": { + "type": "object", + "description": "Updated item data" + } + }, + "required": ["table_name", "key", "item"] + } + ), + Tool( + name="dynamodb_item_delete", + description="Delete an item from a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "key": { + "type": "object", + "description": "Key to identify the item" + } + }, + "required": ["table_name", "key"] + } + ), + Tool( + name="dynamodb_item_query", + description="Query items in a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "key_condition": { + "type": "string", + "description": "Key condition expression" + }, + "expression_values": { + "type": "object", + "description": "Expression attribute values" + } + }, + "required": ["table_name", "key_condition", "expression_values"] + } + ), + Tool( + name="dynamodb_item_scan", + description="Scan items in a DynamoDB table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "filter_expression": { + "type": "string", + "description": "Filter expression" + }, + "expression_attributes": { + "type": "object", + "properties": { + "values": { + "type": "object", + "description": "Expression attribute values" + }, + "names": { + "type": "object", + "description": "Expression attribute names" + } + } + } + }, + "required": ["table_name"] + } + ), + Tool( + name="dynamodb_batch_get", + description="Batch get multiple items from DynamoDB tables", + inputSchema={ + "type": "object", + "properties": { + "request_items": { + "type": "object", + "description": "Map of table names to keys to retrieve", + "additionalProperties": { + "type": "object", + "properties": { + "Keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "ConsistentRead": { + "type": "boolean" + }, + "ProjectionExpression": { + "type": "string" + } + }, + "required": ["Keys"] + } + } + }, + "required": ["request_items"] + } + ), + Tool( + name="dynamodb_item_batch_write", + description="Batch write operations (put/delete) for DynamoDB items", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "operation": { + "type": "string", + "enum": ["put", "delete"], + "description": "Type of batch operation (put or delete)" + }, + "items": { + "type": "array", + "description": "Array of items to process" + }, + "key_attributes": { + "type": "array", + "description": "For delete operations, specify which attributes form the key", + "items": { + "type": "string" + } + } + }, + "required": ["table_name", "operation", "items"] + } + ), + Tool( + name="dynamodb_describe_ttl", + description="Get the TTL settings for a table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + } + }, + "required": ["table_name"] + } + ), + + Tool( + name="dynamodb_update_ttl", + description="Update the TTL settings for a table", + inputSchema={ + "type": "object", + "properties": { + "table_name": { + "type": "string", + "description": "Name of the DynamoDB table" + }, + "ttl_enabled": { + "type": "boolean", + "description": "Whether TTL should be enabled" + }, + "ttl_attribute": { + "type": "string", + "description": "The attribute name to use for TTL" + } + }, + "required": ["table_name", "ttl_enabled", "ttl_attribute"] + } + ), + Tool( + name="dynamodb_batch_execute", + description="Execute multiple PartiQL statements in a batch", + inputSchema={ + "type": "object", + "properties": { + "statements": { + "type": "array", + "description": "List of PartiQL statements to execute", + "items": { + "type": "string" + } + }, + "parameters": { + "type": "array", + "description": "List of parameter lists for each statement", + "items": { + "type": "array" + } + } + }, + "required": ["statements", "parameters"] + } + ), + ] + + +def get_aws_tools() -> list[Tool]: + return [ + *get_s3_tools(), + *get_dynamodb_tools() + ] diff --git a/src/aws/src/mcp_server_aws/utils.py b/src/aws/src/mcp_server_aws/utils.py new file mode 100644 index 00000000..ceaa55d6 --- /dev/null +++ b/src/aws/src/mcp_server_aws/utils.py @@ -0,0 +1,16 @@ +def get_dynamodb_type(value): + if isinstance(value, str): + return {'S': value} + elif isinstance(value, (int, float)): + return {'N': str(value)} + elif isinstance(value, bool): + return {'BOOL': value} + elif value is None: + return {'NULL': True} + elif isinstance(value, list): + return {'L': [get_dynamodb_type(v) for v in value]} + elif isinstance(value, dict): + return {'M': {k: get_dynamodb_type(v) for k, v in value.items()}} + else: + raise ValueError( + f"Unsupported type for DynamoDB: {type(value)}") diff --git a/src/aws/uv.lock b/src/aws/uv.lock new file mode 100644 index 00000000..7c1c7cae --- /dev/null +++ b/src/aws/uv.lock @@ -0,0 +1,327 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "boto3" +version = "1.35.71" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/6d/8e89a60e756c5da4ef56afa738aabed4aa16945676e98b23ede17dffb007/boto3-1.35.71.tar.gz", hash = "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f", size = 111006 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/db/c544d414b6c903011489fc33e2c171d497437b9914a7b587576ee31694b3/boto3-1.35.71-py3-none-any.whl", hash = "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16", size = 139177 }, +] + +[[package]] +name = "botocore" +version = "1.35.71" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/7b/c3f9babe738d5efeb96bd5b250bafcd733c2fd5d8650d8986daa86ee45a1/botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba", size = 13238393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/49/b821048ae671518c00064a936ca08ab4ef7a715d866c3f0331688febeedd/botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1", size = 13034516 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "mcp" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "sse-starlette" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 }, +] + +[[package]] +name = "mcp-server-aws" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "boto3" }, + { name = "mcp" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.35.53" }, + { name = "mcp", specifier = ">=1.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, +] + +[[package]] +name = "pydantic" +version = "2.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "s3transfer" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 }, +] + +[[package]] +name = "starlette" +version = "0.41.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, +]