Repository: awslabs/mcp

Issue:
(aws-api-mcp-server): Commands with `<outfile>` parameter aren't supported
### Describe the bug

Some commands take `<outfile>` parameter. For example:

```
aws lambda invoke --function-name AnalysisLambda --payload file:///Users/aznizam/temp/lambda-invoke-args-base64.txt result.txt

aws s3api get-object --bucket awouters-bucket-1 --key test.txt /tmp/aws-api-mcp/test.txt
```

See https://docs.aws.amazon.com/cli/latest/reference/lambda/invoke.html documentation as an example.

### Expected Behavior

When executing commands with `<outfile>` parameter `call_aws` tool creates a file with streamed content.

### Current Behavior

A validation error is raised with the following message:

> Output file parameters are not supported yet. Use - as the output file to get the requested data in the response.

### Reproduction Steps

You can reproduce by asking the agent to call a Lambda.

### Possible Solution

_No response_

### Additional Information/Context

_No response_

### OS

macOS

### Server

aws-api-mcp-server

### Server Version

_No response_

### Region experiencing the issue

-

### Other information

_No response_

### Service quota

- [x] I have reviewed the service quotas for this construct


Discussion:

Patch:
diff --git a/src/aws-api-mcp-server/CHANGELOG.md b/src/aws-api-mcp-server/CHANGELOG.md
index 8723dc0c14..d6dca4b5f0 100644
--- a/src/aws-api-mcp-server/CHANGELOG.md
+++ b/src/aws-api-mcp-server/CHANGELOG.md
@@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Configurable port for HTTP transport mode via `AWS_API_MCP_PORT` environment variable (defaults to 8000)
 - Configurable host for HTTP transport mode via `AWS_API_MCP_HOST` environment variable (defaults to 127.0.0.1)
 
+### Fixed
+
+- Support commands with outfile parameter (#1154)
+
+## [0.2.8] - 2025-08-21
+
 ### Changed
 
 - Fetch embedding model from AWS instead of Hugging Face (#1127)
diff --git a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py
index 8914533046..cfe1b6cb62 100644
--- a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py
+++ b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py
@@ -14,6 +14,7 @@
 
 import awscli.clidriver
 import re
+from awscli.paramfile import LOCAL_PREFIX_MAP, URIArgumentHandler
 from botocore.model import OperationModel
 from collections.abc import Set
 from loguru import logger
@@ -21,6 +22,19 @@
 from typing import Any, NamedTuple
 
 
+def _deny_remote_prefix(prefix, uri):
+    raise ValueError(f'{prefix} prefix is not allowed')
+
+
+RESTRICTED_URI_HANDLER = URIArgumentHandler(
+    prefixes={
+        **LOCAL_PREFIX_MAP,
+        'http://': (_deny_remote_prefix, {}),
+        'https://': (_deny_remote_prefix, {}),
+    }
+)
+
+
 PaginationConfig = dict[str, int]
 
 
@@ -35,6 +49,7 @@ class ConfigResult(NamedTuple):
 
 driver = awscli.clidriver.create_clidriver()
 session = driver.session
+session.register('load-cli-arg', RESTRICTED_URI_HANDLER)
 
 
 class OperationFilters:
diff --git a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/command.py b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/command.py
index 4d338bd79f..fca7d4fe6f 100644
--- a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/command.py
+++ b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/command.py
@@ -15,10 +15,26 @@
 import dataclasses
 from .command_metadata import CommandMetadata
 from botocore import xform_name
+from botocore.model import OperationModel
 from jmespath.parser import ParsedResult
 from typing import Any
 
 
+@dataclasses.dataclass(frozen=True)
+class OutputFile:
+    """Represents an output file configuration for AWS CLI commands."""
+
+    path: str
+    response_key: str
+
+    @classmethod
+    def from_operation(cls, path: str, operation_model: OperationModel) -> 'OutputFile':
+        """Create OutputFile from operation model for streaming operations."""
+        return cls(
+            path, response_key=getattr(operation_model.output_shape, 'serialization')['payload']
+        )
+
+
 @dataclasses.dataclass(frozen=True)
 class IRCommand:
     """Intermediate representation of an AWS CLI command."""
@@ -29,6 +45,7 @@ class IRCommand:
     profile: str | None = None
     client_side_filter: ParsedResult | None = None
     is_awscli_customization: bool = False
+    output_file: OutputFile | None = None
 
     @property
     def operation_python_name(self):
diff --git a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py
index d0a57f5edb..2b153bc084 100644
--- a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py
+++ b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py
@@ -18,7 +18,7 @@
 from ..aws.services import (
     extract_pagination_config,
 )
-from ..common.command import IRCommand
+from ..common.command import IRCommand, OutputFile
 from ..common.config import OPT_IN_TELEMETRY, READ_OPERATIONS_ONLY_MODE
 from ..common.helpers import operation_timer
 from botocore.config import Config
@@ -27,6 +27,7 @@
 
 
 TIMEOUT_AFTER_SECONDS = 10
+CHUNK_SIZE = 4 * 1024 * 1024
 
 # Get package version for user agent
 try:
@@ -86,6 +87,9 @@ def interpret(
             if client_side_filter is not None:
                 response = _apply_filter(response, client_side_filter)
 
+        if ir.has_streaming_output and ir.output_file and ir.output_file.path != '-':
+            response = _handle_streaming_output(response, ir.output_file)
+
         return response
 
 
@@ -98,6 +102,16 @@ def _get_user_agent_extra() -> str:
     return user_agent_extra
 
 
+def _handle_streaming_output(response: dict[str, Any], output_file: OutputFile) -> dict[str, Any]:
+    streaming_output = response[output_file.response_key]
+    with open(output_file.path, 'wb') as f:
+        for chunk in streaming_output.iter_chunks(chunk_size=CHUNK_SIZE):
+            f.write(chunk)
+
+    del response[output_file.response_key]
+    return response
+
+
 def _apply_filter(response: dict[str, Any], client_side_filter: ParsedResult) -> dict[str, Any]:
     response_metadata = response.get('ResponseMetadata')
     filtered_result = client_side_filter.search(response)
diff --git a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py
index e92188bd9c..8c42756484 100644
--- a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py
+++ b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py
@@ -15,6 +15,7 @@
 import argparse
 import botocore.serialize
 import jmespath
+import os
 import re
 from ..aws.regions import GLOBAL_SERVICE_REGIONS
 from ..aws.services import (
@@ -22,7 +23,7 @@
     get_operation_filters,
     session,
 )
-from ..common.command import IRCommand
+from ..common.command import IRCommand, OutputFile
 from ..common.command_metadata import CommandMetadata
 from ..common.config import AWS_API_MCP_PROFILE_NAME, get_region
 from ..common.errors import (
@@ -65,6 +66,7 @@
 from collections.abc import Generator
 from difflib import SequenceMatcher
 from jmespath.exceptions import ParseError
+from pathlib import Path
 from typing import Any, NamedTuple, cast
 
 
@@ -388,14 +390,6 @@ def _handle_service_command(
     parsed_args = operation_parser.parse_operation_args(command_metadata, service_remaining)
     _handle_invalid_parameters(command_metadata, service, operation, parsed_args)
 
-    outfile = getattr(parsed_args.operation_args, 'outfile', None)
-    if outfile is not None and outfile != '-':
-        # Output file parameters are currently ignored by the interpreter
-        # Raising a validation error to make it explicit
-        raise CommandValidationError(
-            'Output file parameters are not supported yet. Use - as the output file to get the requested data in the response.'
-        )
-
     try:
         parameters = operation_command._build_call_parameters(
             parsed_args.operation_args, operation_command.arg_table
@@ -412,7 +406,9 @@ def _handle_service_command(
         parameters,
     )
 
-    _validate_parameters(parameters, operation_command.arg_table)
+    _validate_parameters(
+        parameters, operation_command.arg_table, operation_command._operation_model
+    )
 
     arn_region = _fetch_region_from_arn(parameters)
     global_args.region = region or arn_region
@@ -422,6 +418,8 @@ def _handle_service_command(
     ):
         global_args.region = GLOBAL_SERVICE_REGIONS[command_metadata.service_sdk_name]
 
+    _validate_output_file(command_metadata, parsed_args)
+
     _validate_request_serialization(
         operation,
         service_command.service_model,
@@ -434,10 +432,13 @@ def _handle_service_command(
         operation,
         parameters,
     )
+
     return _construct_command(
         command_metadata=command_metadata,
         global_args=global_args,
         parameters=parameters,
+        parsed_args=parsed_args,
+        operation_model=operation_command._operation_model,
     )
 
 
@@ -628,21 +629,30 @@ def _validate_global_args(service: str, global_args: argparse.Namespace):
 def _validate_parameters(
     parameters: dict[str, Any],
     arg_table: dict[str, BaseCLIArgument],
+    operation_model: OperationModel,
 ) -> None:
     validator = BotoCoreParamValidator()
-    param_name_to_arg = {
-        arg._serialized_name: arg for arg in arg_table.values() if isinstance(arg, CLIArgument)
+
+    serialized_to_cli = {
+        arg._serialized_name: arg.cli_name
+        for arg in arg_table.values()
+        if isinstance(arg, CLIArgument)
+        and hasattr(arg, '_serialized_name')
+        and hasattr(arg, 'cli_name')
     }
+
     errors = []
+
+    input_shape = operation_model.input_shape
+    boto3_members = getattr(input_shape, 'members')
+
     for key, value in parameters.items():
-        cli_argument = param_name_to_arg.get(key)
-        if not cli_argument or not cli_argument.argument_model:
-            continue
-        report = validator.validate(value, cli_argument.argument_model)
-        if report.has_errors():
-            errors.append(
-                ParameterValidationErrorRecord(cli_argument.cli_name, report.generate_report())
-            )
+        boto3_shape = boto3_members.get(key)
+        if boto3_shape is not None:
+            report = validator.validate(value, boto3_shape)
+            if report.has_errors():
+                cli_name = serialized_to_cli.get(key, key)
+                errors.append(ParameterValidationErrorRecord(cli_name, report.generate_report()))
     if errors:
         raise ParameterSchemaValidationError(errors)
 
@@ -708,6 +718,13 @@ def _validate_request_serialization(
         ) from err
 
 
+def _validate_output_file(command_metadata: CommandMetadata, parsed_args: ParsedOperationArgs):
+    if command_metadata.has_streaming_output:
+        output_file_path = parsed_args.operation_args.outfile
+        if output_file_path != '-' and not os.path.isabs(Path(output_file_path)):
+            raise ValueError(f'{output_file_path} should be an aboslute path')
+
+
 def _fetch_region_from_arn(parameters: dict[str, Any]) -> str | None:
     for param_value in parameters.values():
         if isinstance(param_value, str):
@@ -722,6 +739,8 @@ def _construct_command(
     global_args: argparse.Namespace,
     parameters: dict[str, Any],
     is_awscli_customization: bool = False,
+    parsed_args: ParsedOperationArgs | None = None,
+    operation_model: OperationModel | None = None,
 ) -> IRCommand:
     profile = getattr(global_args, 'profile', None)
     region = (
@@ -744,6 +763,12 @@ def _construct_command(
                 msg=str(error),
             )
 
+    output_file = (
+        OutputFile.from_operation(parsed_args.operation_args.outfile, operation_model)
+        if command_metadata.has_streaming_output and parsed_args and operation_model
+        else None
+    )
+
     return IRCommand(
         command_metadata=command_metadata,
         parameters=parameters,
@@ -751,6 +776,7 @@ def _construct_command(
         profile=profile,
         client_side_filter=client_side_filter,
         is_awscli_customization=is_awscli_customization,
+        output_file=output_file,
     )
 
 
diff --git a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py
index 120c39c504..35747c2e05 100644
--- a/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py
+++ b/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py
@@ -161,11 +161,12 @@ async def suggest_aws_commands(
     - All commands are validated before execution to prevent errors
     - Supports pagination control via max_results parameter
     - The current working directory is {WORKING_DIRECTORY}
+    - File paths should always have forward slash (/) as a separator regardless of the system. Example: 'c:/folder/file.txt'
 
     Best practices for command generation:
-    — Always use the most specific service and operation names
+    - Always use the most specific service and operation names
     - Always use the working directory when writing files, unless user explicitly mentioned another directory
-    — Include --region when operating across regions
+    - Include --region when operating across regions
     - Only use filters (--filters, --query, --prefix, --pattern, etc) when necessary or user explicitly asked for it
 
     Command restrictions:


Test patch:
diff --git a/src/aws-api-mcp-server/tests/aws/test_service.py b/src/aws-api-mcp-server/tests/aws/test_service.py
index 6c598a918d..56d300482e 100644
--- a/src/aws-api-mcp-server/tests/aws/test_service.py
+++ b/src/aws-api-mcp-server/tests/aws/test_service.py
@@ -32,8 +32,11 @@
     CLOUD9_PARAMS_MISSING_CONTEXT_FAILURES,
     EC2_DESCRIBE_INSTANCES,
     GET_CALLER_IDENTITY_PAYLOAD,
+    LAMBDA_INVOKE_PAYLOAD,
+    S3_GET_OBJECT_PAYLOAD,
     SSM_LIST_NODES_PAYLOAD,
     T2_EC2_DESCRIBE_INSTANCES_FILTERED,
+    create_file_open_mock,
     patch_boto3,
 )
 from typing import Any
@@ -539,3 +542,54 @@ def test_profile_not_added_if_present_for_customizations(mock_get_region, mock_m
     assert '--profile' in args
     profile_index = args.index('--profile')
     assert args[profile_index + 1] == 'different'
+
+
+@pytest.mark.parametrize(
+    'command,expected_outfile,expected_content',
+    [
+        (
+            'aws s3api get-object --bucket test-bucket --key test-key /tmp/myfile.template',
+            '/tmp/myfile.template',
+            S3_GET_OBJECT_PAYLOAD['Body'].content,
+        ),
+        (
+            'aws lambda invoke --function-name my-function /tmp/response.json',
+            '/tmp/response.json',
+            LAMBDA_INVOKE_PAYLOAD['Payload'].content,
+        ),
+    ],
+)
+def test_interpret_command_creates_output_file_for_streaming_operations(
+    command, expected_outfile, expected_content
+):
+    """Test that interpret_command writes an output file for streaming operations with outfile parameter."""
+    with patch_boto3():
+        mock_open_side_effect, mock_files = create_file_open_mock(expected_outfile)
+
+        with patch('builtins.open', side_effect=mock_open_side_effect):
+            response = interpret_command(cli_command=command)
+
+            assert response.response is not None
+            assert response.response.status_code == 200
+
+            mock_file = mock_files[expected_outfile]
+            mock_file.write.assert_called_with(expected_content)
+
+            assert response.response.as_json is not None
+            response_data = json.loads(response.response.as_json)
+
+            assert 'Body' not in response_data
+            assert 'Payload' not in response_data
+
+
+@pytest.mark.parametrize(
+    'command',
+    [
+        'aws s3api get-object --bucket test-bucket --key test-key relative/path/file.txt',
+        'aws lambda invoke --function-name my-function response.json',
+    ],
+)
+def test_validate_output_file_raises_error_for_relative_paths(command):
+    """Test that _validate_output_file raises ValueError for streaming operations with relative paths."""
+    with pytest.raises(ValueError, match=r'.* should be an aboslute path'):
+        interpret_command(cli_command=command)
diff --git a/src/aws-api-mcp-server/tests/fixtures.py b/src/aws-api-mcp-server/tests/fixtures.py
index 4846f8bb2d..f8d283b5cf 100644
--- a/src/aws-api-mcp-server/tests/fixtures.py
+++ b/src/aws-api-mcp-server/tests/fixtures.py
@@ -5,7 +5,7 @@
 from .history_handler import history
 from awslabs.aws_api_mcp_server.core.common.models import Credentials
 from copy import deepcopy
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
 
 
 S3_CLI_NO_REGION = 'aws s3api list-buckets'
@@ -176,6 +176,33 @@
 }
 
 
+class MockStreamingBody:
+    """Mock implementation of boto3's StreamingBody for testing."""
+
+    def __init__(self, content):
+        """Initialize the mock streaming body with content."""
+        self.content = content
+
+    def iter_chunks(self, chunk_size=1024):
+        """Yield chunks of the content."""
+        yield self.content
+
+    def read(self):
+        """Read all content."""
+        return self.content
+
+
+S3_GET_OBJECT_PAYLOAD = {
+    'Body': MockStreamingBody(b'test content'),
+    'ResponseMetadata': {'HTTPStatusCode': 200},
+}
+
+LAMBDA_INVOKE_PAYLOAD = {
+    'Payload': MockStreamingBody(b'{"result": "success"}'),
+    'ResponseMetadata': {'HTTPStatusCode': 200},
+}
+
+
 def raise_(ex):
     """Raise the given exception."""
     raise ex
@@ -189,6 +216,8 @@ def raise_(ex):
     'GetCallerIdentity': lambda *args, **kwargs: GET_CALLER_IDENTITY_PAYLOAD,
     'ListBuckets': lambda *args, **kwargs: LIST_BUCKETS_PAYLOAD,
     'ListNodes': lambda *args, **kwargs: SSM_LIST_NODES_PAYLOAD,
+    'GetObject': lambda *args, **kwargs: S3_GET_OBJECT_PAYLOAD,
+    'Invoke': lambda *args, **kwargs: LAMBDA_INVOKE_PAYLOAD,
 }
 
 
@@ -210,6 +239,22 @@ def mock_make_api_call(self, operation_name, kwarg):
     return orig(self, operation_name, kwarg)
 
 
+def create_file_open_mock(*target_files):
+    """Create a mock open function that only mocks specific files."""
+    original_open = open
+    mock_files = {filename: MagicMock() for filename in target_files}
+
+    def mock_open_side_effect(filename, mode='r', *args, **kwargs):
+        if filename in target_files:
+            mock_context = MagicMock()
+            mock_file = mock_files[filename]
+            mock_context.__enter__.return_value = mock_file
+            return mock_context
+        return original_open(filename, mode, *args, **kwargs)
+
+    return mock_open_side_effect, mock_files
+
+
 @contextlib.contextmanager
 def patch_boto3():
     """Context manager to patch boto3 for non-paginated API calls."""
diff --git a/src/aws-api-mcp-server/tests/kb/test_generate_embeddings.py b/src/aws-api-mcp-server/tests/kb/test_generate_embeddings.py
index 4881ee9dd9..8b32999f1a 100644
--- a/src/aws-api-mcp-server/tests/kb/test_generate_embeddings.py
+++ b/src/aws-api-mcp-server/tests/kb/test_generate_embeddings.py
@@ -266,8 +266,13 @@ def test_generate_operation_document_for_custom_argument():
 
 def test_get_aws_api_documents():
     """Test _get_aws_api_documents."""
+    # Create fresh driver to avoid interference from other tests
+    import awscli.clidriver
+
+    new_driver = awscli.clidriver.create_clidriver()
+
     # Get original command table and filter to only s3, cloudformation, lambda, configure, history
-    original_table = driver._get_command_table()
+    original_table = new_driver._get_command_table()
     filtered_table = {
         k: v
         for k, v in original_table.items()
diff --git a/src/aws-api-mcp-server/tests/parser/test_parser.py b/src/aws-api-mcp-server/tests/parser/test_parser.py
index b1f8a42a6a..24b074f76a 100644
--- a/src/aws-api-mcp-server/tests/parser/test_parser.py
+++ b/src/aws-api-mcp-server/tests/parser/test_parser.py
@@ -3,7 +3,6 @@
 from awslabs.aws_api_mcp_server.core.common.command_metadata import CommandMetadata
 from awslabs.aws_api_mcp_server.core.common.errors import (
     ClientSideFilterError,
-    CommandValidationError,
     ExpectedArgumentError,
     InvalidChoiceForParameterError,
     InvalidParametersReceivedError,
@@ -217,25 +216,6 @@ def test_invalid_type_for_parameter(command, message):
         parse(command)
 
 
-@pytest.mark.parametrize(
-    'command, message',
-    [
-        (
-            'aws lambda  update-function-code --function-name MyFunction --zip-file  fileb://newfunction.zip',
-            str(
-                CommandValidationError(
-                    '-zip-file must be a zip file with the fileb:// prefix.\nExample usage:  --zip-file fileb://path/to/file.zip'
-                )
-            ),
-        )
-    ],
-)
-def test_command_validation_error_for_parameter(command, message):
-    """Test that a command validation error is raised for invalid parameters."""
-    with pytest.raises(CommandValidationError, match=message):
-        parse(command)
-
-
 @pytest.mark.parametrize(
     'command, messages',
     [
@@ -606,22 +586,6 @@ def test_client_side_filter_error():
         parse(command)
 
 
-@pytest.mark.parametrize(
-    'command',
-    [
-        'aws s3api get-object --bucket aws-sam-cli-managed-default-samclisourcebucket --key lambda-sqs-sam-test-1/1f1a15295b5529effed491b54a5b5b83.template myfile.template',
-        'aws lambda invoke --function-name my-function response.json',
-    ],
-)
-def test_outfile_parameter_not_supported(command):
-    """Test that outfile parameters raise a validation error."""
-    with pytest.raises(
-        CommandValidationError,
-        match='Output file parameters are not supported yet. Use - as the output file to get the requested data in the response.',
-    ):
-        parse(command)
-
-
 def test_valid_expand_user_home_directory():
     """Test that tilde is replaced with user home directory."""
     result = parse(cli_command='aws s3 cp s3://my_file ~/temp/test.txt')
diff --git a/src/aws-api-mcp-server/tests/parser/test_parser_customizations.py b/src/aws-api-mcp-server/tests/parser/test_parser_customizations.py
index b7e1e927ec..329b16e4b6 100644
--- a/src/aws-api-mcp-server/tests/parser/test_parser_customizations.py
+++ b/src/aws-api-mcp-server/tests/parser/test_parser_customizations.py
@@ -1,6 +1,7 @@
 import pytest
 from awslabs.aws_api_mcp_server.core.common.command import IRCommand
 from awslabs.aws_api_mcp_server.core.common.errors import (
+    CommandValidationError,
     InvalidServiceOperationError,
     MissingRequiredParametersError,
     OperationNotAllowedError,
@@ -11,6 +12,8 @@
     is_denied_custom_operation,
     parse,
 )
+from tests.fixtures import create_file_open_mock
+from unittest.mock import patch
 
 
 def test_wait_is_custom_operation():
@@ -414,3 +417,56 @@ def test_rds_generate_db_auth_token_with_numeric_port():
     assert result.command_metadata.operation_sdk_name == 'generate-db-auth-token'
     assert result.is_awscli_customization is True
     assert result.parameters['--port'] == '5432'  # Port is a string, not an integer
+
+
+def test_local_file_uri():
+    """Test aws command with URI input file parameter."""
+    import io
+    import zipfile
+
+    zip_buffer = io.BytesIO()
+    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
+        zip_file.writestr(
+            'lambda_function.py', 'def lambda_handler(event, context): return "Hello World"'
+        )
+    mock_zip_content = zip_buffer.getvalue()
+
+    zip_file_path = '/test/path/lambda-deployment.zip'
+    mock_open_side_effect, mock_files = create_file_open_mock(zip_file_path)
+
+    with patch('builtins.open', side_effect=mock_open_side_effect):
+        mock_file = mock_files[zip_file_path]
+        mock_file.read.return_value = mock_zip_content
+
+        result = parse(
+            f'aws lambda create-function --function-name hello-world-lambda --runtime python3.9 '
+            f'--role arn:aws:iam::123456789012:role/lambda-test-role --handler lambda_function.lambda_handler '
+            f'--zip-file fileb://{zip_file_path} --description "A Hello World Lambda function"'
+        )
+
+        assert result.is_awscli_customization is False
+        assert result.command_metadata.service_sdk_name == 'lambda'
+        assert result.command_metadata.operation_sdk_name == 'CreateFunction'
+
+        assert 'Code' in result.parameters
+        assert 'ZipFile' in result.parameters['Code']
+        assert isinstance(result.parameters['Code']['ZipFile'], bytes)
+        assert result.parameters['Code']['ZipFile'] == mock_zip_content
+
+        assert result.parameters['FunctionName'] == 'hello-world-lambda'
+        assert result.parameters['Runtime'] == 'python3.9'
+        assert result.parameters['Role'] == 'arn:aws:iam::123456789012:role/lambda-test-role'
+        assert result.parameters['Handler'] == 'lambda_function.lambda_handler'
+        assert result.parameters['Description'] == 'A Hello World Lambda function'
+
+
+def test_http_uri_validation_error():
+    """Test aws command with http:// URI throws validation error."""
+    with pytest.raises(CommandValidationError) as exc_info:
+        parse(
+            'aws cloudformation create-stack --stack-name test-stack '
+            '--template-body http://example.com/template.yaml'
+        )
+
+    error_message = str(exc_info.value)
+    assert 'http:// prefix is not allowed' in error_message