diff --git a/src/aws-healthomics-mcp-server/CHANGELOG.md b/src/aws-healthomics-mcp-server/CHANGELOG.md index 4e1ba6ddff..6a283662cf 100644 --- a/src/aws-healthomics-mcp-server/CHANGELOG.md +++ b/src/aws-healthomics-mcp-server/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- **Output path layout for runs**: `StartAHORun` accepts an optional `output_uri_prefix_mode` (`SERVICE_DEFINED` or `NONE_WITH_OVERWRITE`). `SERVICE_DEFINED`, the HealthOmics default, writes outputs under `//out/`; `NONE_WITH_OVERWRITE` writes them directly under `output_uri`, with logs still under `logs//`, and overwrites same-named files from earlier runs without warning. The value is checked locally and sent only when provided, and `GetAHORun` now returns `outputUriPrefixMode`. Requires boto3/botocore >= 1.43.123 - **Session policies for runs**: `StartAHORun` and `StartAHORunBatch` accept an optional `session_policy` (inline IAM policy as a JSON string or object, max 2048 characters) that scopes down the run role; its shape and length are checked locally and it is sent minified, and `GetAHORun` now returns `sessionPolicy`. Requires boto3/botocore >= 1.43.90 - **StartAHORun tags**: `StartAHORun` now accepts an optional `tags` map that is applied to the run at start - **ListAHORuns date filters**: `created_after` and `created_before` values without a UTC offset, such as `2024-01-01` or `2024-01-01T10:00`, are now treated as UTC instead of failing with "can't compare offset-naive and offset-aware datetimes" diff --git a/src/aws-healthomics-mcp-server/README.md b/src/aws-healthomics-mcp-server/README.md index ab0b0b889e..b717599367 100644 --- a/src/aws-healthomics-mcp-server/README.md +++ b/src/aws-healthomics-mcp-server/README.md @@ -51,9 +51,9 @@ This MCP server provides tools for: ### Workflow Execution Tools -1. **StartAHORun** - Start workflow runs with custom parameters, resource configuration, optional VPC networking mode with a named configuration, optional tags, and an optional inline IAM session policy to scope down the run role +1. **StartAHORun** - Start workflow runs with custom parameters, resource configuration, optional VPC networking mode with a named configuration, optional tags, an optional inline IAM session policy to scope down the run role, and an optional output path layout: `SERVICE_DEFINED` (the default) writes outputs under a run ID folder, while `NONE_WITH_OVERWRITE` writes them directly under the output URI and overwrites same-named files from earlier runs without warning, so use a unique output URI per run unless overwriting is intended 2. **ListAHORuns** - List workflow runs with filtering by status and date ranges -3. **GetAHORun** - Retrieve detailed run information including status and metadata +3. **GetAHORun** - Retrieve detailed run information including status, metadata, and the output path layout when the service returns it 4. **ListAHORunTasks** - List tasks for specific runs with status filtering 5. **GetAHORunTask** - Get detailed information about specific workflow tasks diff --git a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/consts.py b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/consts.py index 53c00d4c62..47e7f4e189 100644 --- a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/consts.py +++ b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/consts.py @@ -89,6 +89,14 @@ RUN_LOG_LEVEL_ALL, ] +# Run output URI prefix modes (controls the output path layout under outputUri) +OUTPUT_URI_PREFIX_MODE_SERVICE_DEFINED = 'SERVICE_DEFINED' +OUTPUT_URI_PREFIX_MODE_NONE_WITH_OVERWRITE = 'NONE_WITH_OVERWRITE' +OUTPUT_URI_PREFIX_MODES = [ + OUTPUT_URI_PREFIX_MODE_SERVICE_DEFINED, + OUTPUT_URI_PREFIX_MODE_NONE_WITH_OVERWRITE, +] + # Run statuses RUN_STATUS_PENDING = 'PENDING' RUN_STATUS_STARTING = 'STARTING' @@ -232,6 +240,7 @@ ERROR_INVALID_CACHE_BEHAVIOR = 'Invalid cache behavior. Must be one of: {}' ERROR_INVALID_SCRATCH_STORAGE_MODE = "Invalid scratch storage mode '{}'. Must be one of: {}" ERROR_INVALID_RUN_LOG_LEVEL = 'Invalid log level. Must be one of: {}' +ERROR_INVALID_OUTPUT_URI_PREFIX_MODE = "'{}' is not a valid value. Must be one of: {}" ERROR_INVALID_SESSION_POLICY = 'Invalid session policy: {}' ERROR_INVALID_RUN_STATUS = 'Invalid run status. Must be one of: {}' ERROR_STATIC_STORAGE_REQUIRES_CAPACITY = ( diff --git a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/run_batch.py b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/run_batch.py index 6290b75a3c..17d110d2a2 100644 --- a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/run_batch.py +++ b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/run_batch.py @@ -117,6 +117,11 @@ async def start_run_batch( ) -> Dict[str, Any]: """Start a new HealthOmics run batch. + Run batches do not support an output path layout option (outputUriPrefixMode). + Batch runs always use the HealthOmics default layout, output_uri//out/. + To write outputs directly under an output URI, start individual runs with + StartAHORun and output_uri_prefix_mode instead. + Args: ctx: MCP context for error reporting workflow_id: ID of the workflow to run diff --git a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/workflow_execution.py b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/workflow_execution.py index b22601de1f..a29232f69d 100644 --- a/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/workflow_execution.py +++ b/src/aws-healthomics-mcp-server/awslabs/aws_healthomics_mcp_server/tools/workflow_execution.py @@ -21,6 +21,7 @@ ERROR_CONFIGURATION_NAME_REQUIRES_VPC_MODE, ERROR_INVALID_CACHE_BEHAVIOR, ERROR_INVALID_NETWORKING_MODE, + ERROR_INVALID_OUTPUT_URI_PREFIX_MODE, ERROR_INVALID_RUN_LOG_LEVEL, ERROR_INVALID_RUN_STATUS, ERROR_INVALID_SCRATCH_STORAGE_MODE, @@ -30,6 +31,8 @@ NETWORKING_MODE_RESTRICTED, NETWORKING_MODE_VPC, NETWORKING_MODES, + OUTPUT_URI_PREFIX_MODE_SERVICE_DEFINED, + OUTPUT_URI_PREFIX_MODES, RUN_LOG_LEVELS, RUN_STATUSES, SCRATCH_STORAGE_MODES, @@ -202,6 +205,19 @@ async def start_run( 'Set to ALL to capture full engine logs for debugging.' ), ), + output_uri_prefix_mode: Optional[str] = Field( + None, + description=( + 'Output path layout for the run. Allowed values: SERVICE_DEFINED, ' + 'NONE_WITH_OVERWRITE. SERVICE_DEFINED (the HealthOmics API default) writes ' + 'outputs under output_uri//out/ and logs under output_uri//logs/. ' + 'NONE_WITH_OVERWRITE writes outputs directly under output_uri with no run ID ' + 'folder, and logs under output_uri/logs//. With NONE_WITH_OVERWRITE, a run into a URI that ' + 'already holds outputs overwrites files with the same name without warning. ' + 'Use a unique output_uri per run unless overwriting is intended. When omitted, ' + 'the HealthOmics API default applies.' + ), + ), tags: Optional[Dict[str, str]] = Field( None, description='Optional tags (key-value pairs) to apply to the run', @@ -249,6 +265,10 @@ async def start_run( log_level: Optional log level for the run (OFF, FATAL, ERROR, or ALL). Controls engine log capture to CloudWatch. Defaults to the HealthOmics API default when omitted. + output_uri_prefix_mode: Optional output path layout (SERVICE_DEFINED or + NONE_WITH_OVERWRITE). NONE_WITH_OVERWRITE writes outputs directly under + output_uri and overwrites same-named files from earlier runs. Defaults to the + HealthOmics API default when omitted. tags: Optional tags (key-value pairs) to apply to the run session_policy: Optional inline IAM session policy (JSON string or dict) that scopes down the permissions of role_arn for this run @@ -343,6 +363,25 @@ async def start_run( 'Invalid log level', ) + # Normalize output_uri_prefix_mode the same way (guards against an unresolved Field default) + effective_output_uri_prefix_mode = ( + output_uri_prefix_mode if isinstance(output_uri_prefix_mode, str) else None + ) + + if ( + effective_output_uri_prefix_mode is not None + and effective_output_uri_prefix_mode not in OUTPUT_URI_PREFIX_MODES + ): + return await handle_tool_error( + ctx, + ValueError( + ERROR_INVALID_OUTPUT_URI_PREFIX_MODE.format( + effective_output_uri_prefix_mode, OUTPUT_URI_PREFIX_MODES + ) + ), + 'Invalid output URI prefix mode', + ) + # Normalize tags the same way (guards against an unresolved Field default) effective_tags = tags if isinstance(tags, dict) else None @@ -393,6 +432,10 @@ async def start_run( if effective_log_level is not None: params['logLevel'] = effective_log_level + # Only set outputUriPrefixMode when provided, to preserve the HealthOmics API default otherwise + if effective_output_uri_prefix_mode is not None: + params['outputUriPrefixMode'] = effective_output_uri_prefix_mode + if effective_tags is not None: params['tags'] = effective_tags @@ -418,6 +461,8 @@ async def start_run( else NETWORKING_MODE_RESTRICTED, 'scratchStorageMode': effective_scratch_storage_mode, 'logLevel': effective_log_level, + 'outputUriPrefixMode': effective_output_uri_prefix_mode + or OUTPUT_URI_PREFIX_MODE_SERVICE_DEFINED, } except Exception as e: return await handle_tool_error(ctx, e, 'Error starting run') @@ -646,7 +691,8 @@ async def get_run( - Basic run information (id, arn, name, status) - Workflow information (workflowId, workflowType, workflowVersionName) - Timing information (creationTime, startTime, stopTime) - - Output locations (outputUri, runOutputUri) + - Output locations (outputUri, runOutputUri) and the output path layout + (outputUriPrefixMode), when the service returns it - IAM role (roleArn) - Run parameters and metadata - Status messages and failure reasons (if applicable) @@ -689,6 +735,7 @@ async def get_run( 'workflowVersionName', 'scratchStorageMode', 'sessionPolicy', + 'outputUriPrefixMode', ]: if field in response: result[field] = response[field] diff --git a/src/aws-healthomics-mcp-server/pyproject.toml b/src/aws-healthomics-mcp-server/pyproject.toml index 4b8e5f26b3..5b10c33fd7 100644 --- a/src/aws-healthomics-mcp-server/pyproject.toml +++ b/src/aws-healthomics-mcp-server/pyproject.toml @@ -5,8 +5,8 @@ description = "An AWS Labs Model Context Protocol (MCP) server for AWS HealthOmi readme = "README.md" requires-python = ">=3.10" dependencies = [ - "boto3>=1.43.90", - "botocore>=1.43.90", + "boto3>=1.43.123", + "botocore>=1.43.123", "loguru>=0.7.0", "mcp[cli]>=2.0.0,<3.0.0", "pydantic>=2.10.6", diff --git a/src/aws-healthomics-mcp-server/tests/test_consts.py b/src/aws-healthomics-mcp-server/tests/test_consts.py index 343d08907d..a0dc6a8859 100644 --- a/src/aws-healthomics-mcp-server/tests/test_consts.py +++ b/src/aws-healthomics-mcp-server/tests/test_consts.py @@ -315,3 +315,17 @@ def test_run_statuses_completeness(self): def test_default_region_in_supported_regions(self): """Test that DEFAULT_REGION is included in HEALTHOMICS_SUPPORTED_REGIONS.""" assert consts.DEFAULT_REGION in consts.HEALTHOMICS_SUPPORTED_REGIONS + + +class TestOutputUriPrefixModes: + """Test cases for the run output URI prefix mode constants and error template.""" + + def test_output_uri_prefix_modes_list(self): + """Test OUTPUT_URI_PREFIX_MODES contains exactly the two API values in order.""" + assert consts.OUTPUT_URI_PREFIX_MODE_SERVICE_DEFINED == 'SERVICE_DEFINED' + assert consts.OUTPUT_URI_PREFIX_MODE_NONE_WITH_OVERWRITE == 'NONE_WITH_OVERWRITE' + assert consts.OUTPUT_URI_PREFIX_MODES == ['SERVICE_DEFINED', 'NONE_WITH_OVERWRITE'] + + def test_error_template_has_two_placeholders(self): + """Test the template exposes placeholders for the value and allowed list.""" + assert consts.ERROR_INVALID_OUTPUT_URI_PREFIX_MODE.count('{}') == 2 diff --git a/src/aws-healthomics-mcp-server/tests/test_sdk_output_uri_prefix_mode.py b/src/aws-healthomics-mcp-server/tests/test_sdk_output_uri_prefix_mode.py new file mode 100644 index 0000000000..9d6d6e06f6 --- /dev/null +++ b/src/aws-healthomics-mcp-server/tests/test_sdk_output_uri_prefix_mode.py @@ -0,0 +1,110 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Smoke tests for AWS SDK output URI prefix mode support. + +These are low-iteration smoke tests (not property tests) that verify the +external AWS SDK behavior required by this feature: + +- The installed boto3 / botocore versions meet the minimum that introduced the + ``outputUriPrefixMode`` field, and pyproject.toml declares that minimum. +- A real ``omics`` boto3 client's StartRun input shape accepts + ``outputUriPrefixMode`` with the values the tool validates against, and the + GetRun output shape returns it (no network calls are made; the fields are + verified against the client's service model). +""" + +import boto3 +import botocore +import pathlib +import re +from awslabs.aws_healthomics_mcp_server.consts import OUTPUT_URI_PREFIX_MODES +from packaging.version import Version + + +# Minimum SDK version whose omics model includes outputUriPrefixMode on StartRun and GetRun. +MINIMUM_SDK_VERSION = Version('1.43.123') + +# Repo root is the parent of the tests/ directory. +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +PYPROJECT_PATH = REPO_ROOT / 'pyproject.toml' + + +def test_installed_boto3_version_meets_minimum(): + """The installed boto3 version is >= 1.43.123.""" + assert Version(boto3.__version__) >= MINIMUM_SDK_VERSION, ( + f'Installed boto3 {boto3.__version__} is below the required minimum {MINIMUM_SDK_VERSION}' + ) + + +def test_installed_botocore_version_meets_minimum(): + """The installed botocore version is >= 1.43.123.""" + assert Version(botocore.__version__) >= MINIMUM_SDK_VERSION, ( + f'Installed botocore {botocore.__version__} is below the required ' + f'minimum {MINIMUM_SDK_VERSION}' + ) + + +def _declared_minimum(package): + """Return the declared minimum Version for a package from pyproject.toml.""" + text = PYPROJECT_PATH.read_text(encoding='utf-8') + match = re.search(rf'["\']{package}\s*>=\s*([0-9][0-9A-Za-z.\-]*)["\']', text) + if match: + return Version(match.group(1)) + return None + + +def test_pyproject_declares_boto3_minimum(): + """pyproject.toml declares a boto3 minimum of at least 1.43.123.""" + declared = _declared_minimum('boto3') + + assert declared is not None, 'boto3 dependency constraint not found in pyproject.toml' + assert declared >= MINIMUM_SDK_VERSION, ( + f'pyproject.toml declares boto3>={declared}, below required {MINIMUM_SDK_VERSION}' + ) + + +def test_pyproject_declares_botocore_minimum(): + """pyproject.toml declares an explicit botocore minimum of at least 1.43.123.""" + declared = _declared_minimum('botocore') + + assert declared is not None, 'botocore dependency constraint not found in pyproject.toml' + assert declared >= MINIMUM_SDK_VERSION, ( + f'pyproject.toml declares botocore>={declared}, below required {MINIMUM_SDK_VERSION}' + ) + + +def _omics_operation_model(operation_name): + """Return the omics operation model (no network calls).""" + client = boto3.client('omics', region_name='us-east-1') + return client.meta.service_model.operation_model(operation_name) + + +def test_start_run_accepts_output_uri_prefix_mode(): + """StartRun accepts outputUriPrefixMode, and its enum matches the tool's allowed values.""" + members = _omics_operation_model('StartRun').input_shape.members + assert 'outputUriPrefixMode' in members, ( + 'StartRun input shape does not declare outputUriPrefixMode; ' + 'the installed SDK does not support output URI prefix mode' + ) + assert sorted(members['outputUriPrefixMode'].enum) == sorted(OUTPUT_URI_PREFIX_MODES) + + +def test_get_run_returns_output_uri_prefix_mode(): + """The GetRun output shape declares outputUriPrefixMode.""" + members = _omics_operation_model('GetRun').output_shape.members + assert 'outputUriPrefixMode' in members, ( + 'GetRun output shape does not declare outputUriPrefixMode; ' + 'the installed SDK does not support output URI prefix mode' + ) diff --git a/src/aws-healthomics-mcp-server/tests/test_workflow_execution.py b/src/aws-healthomics-mcp-server/tests/test_workflow_execution.py index bb2bc9a623..713ec073c3 100644 --- a/src/aws-healthomics-mcp-server/tests/test_workflow_execution.py +++ b/src/aws-healthomics-mcp-server/tests/test_workflow_execution.py @@ -3126,3 +3126,163 @@ async def test_start_run_params_validate_against_botocore_model(self): params = {'requestId': 'test-token', **mock_client.start_run.call_args.kwargs} report = ParamValidator().validate(params, input_shape) assert not report.has_errors(), report.generate_report() + + +class TestStartRunOutputUriPrefixMode: + """start_run and get_run handling of the optional output URI prefix mode. + + - When output_uri_prefix_mode is omitted, no outputUriPrefixMode key is forwarded, so + the HealthOmics API default (SERVICE_DEFINED) applies. + - A valid value is forwarded to the API and reflected in the response. + - An invalid value returns a validation error and the API is not called. + - get_run returns outputUriPrefixMode only when the API response contains it. + """ + + _base_params = { + 'workflow_id': 'wfl-12345', + 'role_arn': 'arn:aws:iam::123456789012:role/HealthOmicsRole', + 'name': 'test-run', + 'output_uri': 's3://my-bucket/outputs/', + 'parameters': {'param1': 'value1'}, + } + + _api_response = { + 'id': 'run-12345', + 'arn': 'arn:aws:omics:us-east-1:123456789012:run/run-12345', + 'status': 'PENDING', + 'uuid': 'uuid-abc-123', + } + + async def _call(self, mock_client, **kwargs): + wrapper = MCPToolTestWrapper(start_run) + with patch( + 'awslabs.aws_healthomics_mcp_server.tools.workflow_execution.get_omics_client', + return_value=mock_client, + ): + return await wrapper.call(ctx=AsyncMock(), **self._base_params, **kwargs) + + @pytest.mark.asyncio + async def test_start_run_omits_output_uri_prefix_mode_when_none(self): + """output_uri_prefix_mode=None => no outputUriPrefixMode key in the API params.""" + mock_client = MagicMock() + mock_client.start_run.return_value = dict(self._api_response) + + result = await self._call(mock_client, output_uri_prefix_mode=None) + + assert 'error' not in result, f'Unexpected error: {result}' + call_kwargs = mock_client.start_run.call_args.kwargs + assert 'outputUriPrefixMode' not in call_kwargs + assert result['outputUriPrefixMode'] == 'SERVICE_DEFINED' + + @pytest.mark.asyncio + async def test_start_run_omits_output_uri_prefix_mode_when_unresolved_field(self): + """An unresolved Field default is treated as omitted, not forwarded or rejected.""" + from pydantic import Field + + mock_client = MagicMock() + mock_client.start_run.return_value = dict(self._api_response) + + result = await self._call(mock_client, output_uri_prefix_mode=Field(None)) + + assert 'error' not in result, f'Unexpected error: {result}' + assert 'outputUriPrefixMode' not in mock_client.start_run.call_args.kwargs + assert result['outputUriPrefixMode'] == 'SERVICE_DEFINED' + + @pytest.mark.parametrize('mode', ['SERVICE_DEFINED', 'NONE_WITH_OVERWRITE']) + @pytest.mark.asyncio + async def test_start_run_forwards_valid_output_uri_prefix_mode(self, mode): + """Each valid value is forwarded to the API and reflected in the response.""" + mock_client = MagicMock() + mock_client.start_run.return_value = dict(self._api_response) + + result = await self._call(mock_client, output_uri_prefix_mode=mode) + + assert 'error' not in result, f'Unexpected error for mode={mode!r}: {result}' + mock_client.start_run.assert_called_once() + assert mock_client.start_run.call_args.kwargs['outputUriPrefixMode'] == mode + assert result['outputUriPrefixMode'] == mode + + @pytest.mark.parametrize('mode', ['INVALID', 'none_with_overwrite', '', 'NONE']) + @pytest.mark.asyncio + async def test_start_run_invalid_output_uri_prefix_mode_returns_error(self, mode): + """An invalid value returns a validation error naming it and never calls the API.""" + mock_client = MagicMock() + + result = await self._call(mock_client, output_uri_prefix_mode=mode) + + assert 'error' in result + assert f"'{mode}'" in result['error'] + assert 'NONE_WITH_OVERWRITE' in result['error'] + assert result['error'].count('Invalid output URI prefix mode') == 1 + mock_client.start_run.assert_not_called() + + def test_start_run_output_uri_prefix_mode_description_documents_overwrite(self): + """The parameter description names both values and the overwrite risk.""" + field_info = ( + _inspect.signature(_workflow_execution.start_run) + .parameters['output_uri_prefix_mode'] + .default + ) + description = field_info.description + + assert field_info.default is None + assert 'SERVICE_DEFINED' in description + assert 'NONE_WITH_OVERWRITE' in description + assert 'overwrites' in description + assert 'unique output_uri per run' in description + + @pytest.mark.asyncio + async def test_start_run_params_validate_against_botocore_model(self): + """The forwarded params, including outputUriPrefixMode, satisfy the real StartRun shape.""" + import botocore.session + from botocore.validate import ParamValidator + + mock_client = MagicMock() + mock_client.start_run.return_value = dict(self._api_response) + + await self._call(mock_client, output_uri_prefix_mode='NONE_WITH_OVERWRITE') + + input_shape = ( + botocore.session.get_session() + .get_service_model('omics') + .operation_model('StartRun') + .input_shape + ) + # requestId is an idempotency token that boto3 auto-populates on real clients + params = {'requestId': 'test-token', **mock_client.start_run.call_args.kwargs} + report = ParamValidator().validate(params, input_shape) + assert not report.has_errors(), report.generate_report() + + @pytest.mark.parametrize('mode', ['SERVICE_DEFINED', 'NONE_WITH_OVERWRITE']) + @pytest.mark.asyncio + async def test_get_run_surfaces_output_uri_prefix_mode(self, mode): + """get_run includes outputUriPrefixMode unchanged when the API returns it.""" + mock_client = MagicMock() + mock_client.get_run.return_value = { + 'id': 'run-12345', + 'status': 'RUNNING', + 'outputUriPrefixMode': mode, + } + + with patch( + 'awslabs.aws_healthomics_mcp_server.tools.workflow_execution.get_omics_client', + return_value=mock_client, + ): + result = await get_run(AsyncMock(), run_id='run-12345') + + assert result['outputUriPrefixMode'] == mode + + @pytest.mark.asyncio + async def test_get_run_omits_output_uri_prefix_mode_when_absent(self): + """get_run omits outputUriPrefixMode when the API response does not contain it.""" + mock_client = MagicMock() + mock_client.get_run.return_value = {'id': 'run-12345', 'status': 'RUNNING'} + + with patch( + 'awslabs.aws_healthomics_mcp_server.tools.workflow_execution.get_omics_client', + return_value=mock_client, + ): + result = await get_run(AsyncMock(), run_id='run-12345') + + assert 'error' not in result, f'Unexpected error: {result}' + assert 'outputUriPrefixMode' not in result diff --git a/src/aws-healthomics-mcp-server/uv.lock b/src/aws-healthomics-mcp-server/uv.lock index 3dc94d37b7..d0c200cf7a 100644 --- a/src/aws-healthomics-mcp-server/uv.lock +++ b/src/aws-healthomics-mcp-server/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -109,8 +109,8 @@ dev = [ [package.metadata] requires-dist = [ - { name = "boto3", specifier = ">=1.43.90" }, - { name = "botocore", specifier = ">=1.43.90" }, + { name = "boto3", specifier = ">=1.43.123" }, + { name = "botocore", specifier = ">=1.43.123" }, { name = "coloredlogs", specifier = ">=15.0" }, { name = "cwltool", extras = ["deps"], specifier = ">=3.1.0" }, { name = "isodate", specifier = ">=0.6.0" }, @@ -175,30 +175,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.110" +version = "1.43.123" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/6e/fe973c8ce8fe4f59d6df6128600b9cc7c4ed75788bed5f5f615f88379529/boto3-1.43.110.tar.gz", hash = "sha256:0251b67d99cc0e7b958db48e7dd149094d453377fcd03299fde7329f975db2e6", size = 112658, upload-time = "2026-10-08T19:30:47.85Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/8a/64b46f075149f9160da44aa7e1052dd18431b9ad4435beb0cfb02cc48624/boto3-1.43.123.tar.gz", hash = "sha256:055fabb990c5d2f31fce4ca569255bd640cdd11a3ca68c9ecad35f4f8a5b83b5", size = 112638, upload-time = "2026-10-30T19:18:50.085Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/97/3295f3f87138c3647be183e08b4f585ae26e794e7ae9f3c44c24880483bd/boto3-1.43.110-py3-none-any.whl", hash = "sha256:74b811e6def4b30d685d4539a9ead3796f0e03a2112f00134e0f5e583ad8b098", size = 140042, upload-time = "2026-10-08T19:30:46.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/a7986c327f80aa6f1544f4cb8f2fa7da4e1fccd472379a21cad0508b8a0e/boto3-1.43.123-py3-none-any.whl", hash = "sha256:d18d66e1e13faa0c25d0b1b8c5ce946e29fb5fc0b6456b655a93a678ff4cc313", size = 140044, upload-time = "2026-10-30T19:18:47.734Z" }, ] [[package]] name = "botocore" -version = "1.43.110" +version = "1.43.123" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/d8/7a2320aabf1b1e62580e990f8a52760e396f4ec56e3ebcbce07d23acc031/botocore-1.43.110.tar.gz", hash = "sha256:888cd54d59502e2195dac30805e5c08dd1be5416932a3793373e81b8f463e006", size = 16317803, upload-time = "2026-10-08T19:30:43.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/011fc2a47fb066c47064b122498d2fd21f55bc90ea514acbddc37ca0f8c1/botocore-1.43.123.tar.gz", hash = "sha256:80948cb9b1cdf1a1a25809089a42e3a1bc4af0258d198222db6e97dcc660dd0c", size = 16486269, upload-time = "2026-10-30T19:18:44.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/6c/bea5940931819d2f8e4a467af8b40302846183bf1e280e31c5a4727c8476/botocore-1.43.110-py3-none-any.whl", hash = "sha256:9f5ae5685b4d7129e32f6084ae8683d5857a1d60d77528b8da77bb53b9dff48f", size = 16015248, upload-time = "2026-10-08T19:30:38.172Z" }, + { url = "https://files.pythonhosted.org/packages/db/96/4f2dffd304425c07160ab2c12cb13322d852fd956fe9d4386228c93f02ea/botocore-1.43.123-py3-none-any.whl", hash = "sha256:e30580f9c1282c321529238ff8ff75066096a2adbd0a293c8ae592c21d1733a3", size = 16185987, upload-time = "2026-10-30T19:18:39.794Z" }, ] [[package]] @@ -1154,8 +1154,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", version = "4.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "httpx2", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')" }, - { name = "httpx2", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten')" }, + { name = "httpx2", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')" }, + { name = "httpx2", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.14' and sys_platform != 'emscripten') or (python_full_version >= '3.12' and sys_platform == 'emscripten')" }, { name = "jsonschema" }, { name = "mcp-types" }, { name = "opentelemetry-api" },