Skip to main content
Glama

Keboola Explorer MCP Server

test_tools.py35.4 kB
from typing import Any, Callable import pytest from mcp.server.fastmcp import Context from pytest_mock import MockerFixture from keboola_mcp_server.clients.client import KeboolaClient from keboola_mcp_server.links import Link from keboola_mcp_server.tools.components import ( add_config_row, create_config, create_sql_transformation, get_config, get_config_examples, list_configs, update_config, update_config_row, update_sql_transformation, ) from keboola_mcp_server.tools.components.model import ( ComponentSummary, ComponentType, ComponentWithConfigurations, ConfigParamRemove, ConfigParamReplace, ConfigParamSet, ConfigParamUpdate, ConfigToolOutput, Configuration, ConfigurationRootSummary, ConfigurationSummary, ListConfigsOutput, ) from keboola_mcp_server.tools.components.utils import TransformationConfiguration, clean_bucket_name from keboola_mcp_server.workspace import WorkspaceManager @pytest.fixture def assert_retrieve_components() -> Callable[ [ ListConfigsOutput, list[dict[str, Any]], list[dict[str, Any]], ], None, ]: """Assert that the _retrieve_components_in_project tool returns the correct components and configurations.""" def _assert_retrieve_components( result: ListConfigsOutput, components: list[dict[str, Any]], configurations: list[dict[str, Any]], ): components_with_configurations = result.components_with_configurations assert len(components_with_configurations) == len(components) # assert basics assert all(isinstance(component, ComponentWithConfigurations) for component in components_with_configurations) assert all(isinstance(component.component, ComponentSummary) for component in components_with_configurations) assert all(isinstance(component.configurations, list) for component in components_with_configurations) assert all( all(isinstance(config, ConfigurationSummary) for config in component.configurations) for component in components_with_configurations ) # assert component list details assert all( returned.component.component_id == expected['id'] for returned, expected in zip(components_with_configurations, components) ) assert all( returned.component.component_name == expected['name'] for returned, expected in zip(components_with_configurations, components) ) assert all( returned.component.component_type == expected['type'] for returned, expected in zip(components_with_configurations, components) ) assert all(not hasattr(returned.component, 'version') for returned in components_with_configurations) # assert configurations list details assert all(len(component.configurations) == len(configurations) for component in components_with_configurations) assert all( all(isinstance(config.configuration_root, ConfigurationRootSummary) for config in component.configurations) for component in components_with_configurations ) # use zip to iterate over the result and mock_configurations since we artificially mock the .get method assert all( all( config.configuration_root.configuration_id == expected['id'] for config, expected in zip(component.configurations, configurations) ) for component in components_with_configurations ) assert all( all( config.configuration_root.name == expected['name'] for config, expected in zip(component.configurations, configurations) ) for component in components_with_configurations ) return _assert_retrieve_components @pytest.mark.asyncio @pytest.mark.parametrize( ('component_types', 'expected_types', 'expected_mock_comp_idxs'), [ # No filter - should retrieve all component types (including transformation) # Order: application, extractor, transformation, writer ([], ['application', 'extractor', 'transformation', 'writer'], [2, 0, 3, 1]), # Single type - extractor only (['extractor'], ['extractor'], [0]), # Single type - writer only (['writer'], ['writer'], [1]), # Single type - application only (['application'], ['application'], [2]), # Single type - transformation only (['transformation'], ['transformation'], [3]), # Multiple types - extractor and writer # Order: extractor, writer (['extractor', 'writer'], ['extractor', 'writer'], [0, 1]), # Multiple types - extractor, writer, and application # Order: application, extractor, writer (['extractor', 'writer', 'application'], ['application', 'extractor', 'writer'], [2, 0, 1]), ], ) async def test_list_configs_by_types( mocker: MockerFixture, mcp_context_components_configs: Context, mock_components: list[dict[str, Any]], mock_configurations: list[dict[str, Any]], assert_retrieve_components: Callable[[ListConfigsOutput, list[dict[str, Any]], list[dict[str, Any]]], None], component_types: list[ComponentType], expected_types: list[ComponentType], expected_mock_comp_idxs: list[int], ): """ Test list_configs when component types are provided with various filters. The expected_mock_comp_idxs are the indices of mock_components that should be returned. """ context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) # Create a mapping of component_type to the matching mock component component_type_map = {comp['type']: comp for comp in mock_components} # Create a side_effect function that returns the correct component based on component_type async def mock_component_list(component_type: ComponentType, include: list[str] | None = None): # Return matching component or empty list if no transformation exists if component_type in component_type_map: return [{**component_type_map[component_type], 'configurations': mock_configurations}] return [] keboola_client.storage_client.component_list = mocker.AsyncMock(side_effect=mock_component_list) result = await list_configs(ctx=context, component_types=component_types) # Get the expected components based on the indices expected_components = [mock_components[i] for i in expected_mock_comp_idxs] assert_retrieve_components(result, expected_components, mock_configurations) # Verify the calls were made with the correct arguments (in sorted order) expected_calls = [mocker.call(component_type=comp_type, include=['configuration']) for comp_type in expected_types] keboola_client.storage_client.component_list.assert_has_calls(expected_calls) @pytest.mark.asyncio async def test_list_configs_from_ids( mocker: MockerFixture, mcp_context_components_configs: Context, mock_configurations: list[dict[str, Any]], mock_component: dict[str, Any], assert_retrieve_components: Callable[[ListConfigsOutput, list[dict[str, Any]], list[dict[str, Any]]], None], ): """Test list_configs when component IDs are provided.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) keboola_client.storage_client.configuration_list = mocker.AsyncMock(return_value=mock_configurations) keboola_client.storage_client.component_detail = mocker.AsyncMock(return_value=mock_component) result = await list_configs(context, component_ids=[mock_component['id']]) assert_retrieve_components(result, [mock_component], mock_configurations) # Verify the calls were made with the correct arguments keboola_client.storage_client.configuration_list.assert_called_once_with(component_id=mock_component['id']) keboola_client.storage_client.component_detail.assert_called_once_with(component_id=mock_component['id']) @pytest.mark.asyncio async def test_get_config( mocker: MockerFixture, mcp_context_components_configs: Context, mock_configuration: dict[str, Any], mock_component: dict[str, Any], mock_metadata: list[dict[str, Any]], ): """Test get_config tool.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) mock_ai_service = mocker.MagicMock() mock_ai_service.get_component_detail = mocker.AsyncMock(return_value=mock_component) keboola_client.ai_service_client = mock_ai_service # mock the configuration_detail method to return the mock_configuration # simulate the response from the API keboola_client.storage_client.configuration_detail = mocker.AsyncMock( return_value={**mock_configuration, 'component': mock_component, 'configurationMetadata': mock_metadata} ) result = await get_config( component_id=mock_component['id'], configuration_id=mock_configuration['id'], ctx=context, ) assert isinstance(result, Configuration) assert result.configuration_root.configuration_id == mock_configuration['id'] assert result.configuration_root.name == mock_configuration['name'] assert result.component is not None assert result.component.component_id == mock_component['id'] assert result.component.component_name == mock_component['name'] assert set(result.links) == { Link( type='ui-detail', title=f'Configuration: {mock_configuration["name"]}', url=( f'https://connection.test.keboola.com/admin/projects/69420/components/' f'{mock_component["id"]}/{mock_configuration["id"]}' ), ), Link( type='ui-dashboard', title=f'{mock_component["id"]} Configurations Dashboard', url=f'https://connection.test.keboola.com/admin/projects/69420/components/{mock_component["id"]}', ), } # Verify the calls were made with the correct arguments keboola_client.storage_client.configuration_detail.assert_called_once_with( component_id=mock_component['id'], configuration_id=mock_configuration['id'] ) @pytest.mark.parametrize( ('sql_dialect', 'expected_component_id', 'expected_configuration_id'), [ ('Snowflake', 'keboola.snowflake-transformation', '1234'), ('BigQuery', 'keboola.google-bigquery-transformation', '5678'), ], ) @pytest.mark.asyncio async def test_create_sql_transformation( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], mock_configuration: dict[str, Any], sql_dialect: str, expected_component_id: str, expected_configuration_id: str, ): """Test create_sql_transformation tool.""" context = mcp_context_components_configs # Mock the WorkspaceManager workspace_manager = WorkspaceManager.from_state(context.session.state) workspace_manager.get_sql_dialect = mocker.AsyncMock(return_value=sql_dialect) # Mock the KeboolaClient keboola_client = KeboolaClient.from_state(context.session.state) component = mock_component component['id'] = expected_component_id configuration = mock_configuration configuration['id'] = expected_configuration_id # Set up the mock for ai_service_client keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=component) keboola_client.storage_client.configuration_create = mocker.AsyncMock(return_value=configuration) transformation_name = mock_configuration['name'] bucket_name = clean_bucket_name(transformation_name) description = mock_configuration['description'] code_blocks = [ TransformationConfiguration.Parameters.Block.Code(name='Code 0', sql_statements=['SELECT * FROM test']), TransformationConfiguration.Parameters.Block.Code(name='Code 1', sql_statements=['SELECT * FROM test2']), ] created_table_name = 'test_table_1' # Test the create_sql_transformation tool new_transformation_configuration = await create_sql_transformation( ctx=context, name=transformation_name, description=description, sql_code_blocks=code_blocks, created_table_names=[created_table_name], ) assert isinstance(new_transformation_configuration, ConfigToolOutput) assert new_transformation_configuration.component_id == expected_component_id assert new_transformation_configuration.configuration_id == mock_configuration['id'] assert new_transformation_configuration.description == mock_configuration['description'] assert new_transformation_configuration.version == mock_configuration['version'] keboola_client.storage_client.configuration_create.assert_called_once_with( component_id=expected_component_id, name=transformation_name, description=description, configuration={ 'parameters': { 'blocks': [ { 'name': 'Blocks', 'codes': [{'name': code.name, 'script': code.sql_statements} for code in code_blocks], } ] }, 'storage': { 'input': {'tables': []}, 'output': { 'tables': [ { 'source': created_table_name, 'destination': f'out.c-{bucket_name}.{created_table_name}', } ] }, }, }, ) @pytest.mark.parametrize('sql_dialect', ['Unknown']) @pytest.mark.asyncio async def test_create_sql_transformation_fail( mocker: MockerFixture, sql_dialect: str, mcp_context_components_configs: Context, ): """Test create_sql_transformation tool which should raise an error if the sql dialect is unknown.""" context = mcp_context_components_configs workspace_manager = WorkspaceManager.from_state(context.session.state) workspace_manager.get_sql_dialect = mocker.AsyncMock(return_value=sql_dialect) with pytest.raises(ValueError, match='Unsupported SQL dialect'): _ = await create_sql_transformation( ctx=context, name='test_name', description='test_description', sql_code_blocks=[ TransformationConfiguration.Parameters.Block.Code(name='Code 0', sql_statements=['SELECT * FROM test']) ], ) @pytest.mark.parametrize( ('sql_dialect', 'expected_component_id', 'parameters', 'storage', 'expected_config'), [ pytest.param( 'Snowflake', 'keboola.snowflake-transformation', {'blocks': [{'name': 'Blocks', 'codes': [{'name': 'Code 0', 'script': ['SELECT * FROM test']}]}]}, {'output': {'tables': []}}, { 'parameters': { 'blocks': [{'name': 'Blocks', 'codes': [{'name': 'Code 0', 'script': ['SELECT * FROM test']}]}] }, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='snowflake_both_provided', ), pytest.param( 'BigQuery', 'keboola.google-bigquery-transformation', {'blocks': [{'name': 'Blocks', 'codes': [{'name': 'Code 0', 'script': ['SELECT * FROM test']}]}]}, None, { 'parameters': { 'blocks': [{'name': 'Blocks', 'codes': [{'name': 'Code 0', 'script': ['SELECT * FROM test']}]}] }, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, id='bigquery_parameters_only', ), pytest.param( 'Snowflake', 'keboola.snowflake-transformation', None, {'output': {'tables': []}}, { 'parameters': { 'blocks': [{'name': 'Existing', 'codes': [{'name': 'Existing Code', 'script': ['SELECT 1']}]}] }, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='snowflake_storage_only', ), ], ) @pytest.mark.asyncio async def test_update_sql_transformation( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], mock_configuration: dict[str, Any], sql_dialect: str, expected_component_id: str, parameters: dict[str, Any] | None, storage: dict[str, Any] | None, expected_config: dict[str, Any], ): """Test update_sql_transformation tool.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) # Mock the WorkspaceManager workspace_manager = WorkspaceManager.from_state(context.session.state) workspace_manager.get_sql_dialect = mocker.AsyncMock(return_value=sql_dialect) existing_configuration = { 'id': mock_configuration['id'], 'name': 'Existing Transformation', 'description': 'Existing description', 'configuration': { 'parameters': { 'blocks': [{'name': 'Existing', 'codes': [{'name': 'Existing Code', 'script': ['SELECT 1']}]}] }, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, 'version': 1, } new_change_description = 'Test transformation update' updated_configuration = mock_configuration.copy() updated_configuration['version'] = 2 mock_component['id'] = expected_component_id keboola_client.storage_client.configuration_detail = mocker.AsyncMock(return_value=existing_configuration) keboola_client.storage_client.configuration_update = mocker.AsyncMock(return_value=updated_configuration) keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) # Convert parameters dict to TransformationConfiguration.Parameters if provided transformation_parameters = None if parameters is not None: transformation_parameters = TransformationConfiguration.Parameters.model_validate(parameters) updated_result = await update_sql_transformation( context, mock_configuration['id'], new_change_description, parameters=transformation_parameters, storage=storage, updated_description=str(), is_disabled=False, ) assert isinstance(updated_result, ConfigToolOutput) assert updated_result.component_id == expected_component_id assert updated_result.configuration_id == mock_configuration['id'] assert updated_result.description == mock_configuration['description'] assert updated_result.version == updated_configuration['version'] keboola_client.ai_service_client.get_component_detail.assert_called_with(component_id=expected_component_id) keboola_client.storage_client.configuration_update.assert_called_once_with( component_id=expected_component_id, configuration_id=mock_configuration['id'], change_description=new_change_description, configuration=expected_config, updated_description='', is_disabled=False, ) @pytest.mark.asyncio async def test_get_config_examples( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], ): context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) # Setup mock to return test data keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) text = await get_config_examples(component_id='keboola.ex-aws-s3', ctx=context) assert ( text == """# Configuration Examples for `keboola.ex-aws-s3` ## Root Configuration Examples 1. Root Configuration: ```json { "foo": "root" } ``` ## Row Configuration Examples 1. Row Configuration: ```json { "foo": "row" } ``` """ ) @pytest.mark.asyncio async def test_create_config( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], mock_configuration: dict[str, Any], ): """Test create_component_root_configuration tool.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) component_id = mock_component['id'] configuration = mock_configuration configuration['id'] = 'test-config-id' # Set up the mock for ai_service_client and storage_client keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) keboola_client.storage_client.configuration_create = mocker.AsyncMock(return_value=configuration) keboola_client.storage_client.configuration_metadata_update = mocker.AsyncMock() name = 'Test Configuration' description = 'Test configuration description' parameters = {'test_param': 'test_value'} storage = {'input': {'tables': []}} # Test the create_component_root_configuration tool result = await create_config( ctx=context, name=name, description=description, component_id=component_id, parameters=parameters, storage=storage, ) assert isinstance(result, ConfigToolOutput) assert result.component_id == component_id assert result.configuration_id == configuration['id'] assert result.description == description assert result.success is True assert result.timestamp is not None assert result.version == configuration['version'] keboola_client.ai_service_client.get_component_detail.assert_called_once_with(component_id=component_id) keboola_client.storage_client.configuration_create.assert_called_once_with( component_id=component_id, name=name, description=description, configuration={'storage': storage, 'parameters': parameters}, ) @pytest.mark.asyncio async def test_add_config_row( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], mock_configuration: dict[str, Any], ): """Test create_component_row_configuration tool.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) component_id = mock_component['id'] configuration_id = 'test-config-id' row_configuration = {'id': 'test-row-id', 'name': 'Test Row', 'version': 1} # Set up the mock for ai_service_client and storage_client keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) keboola_client.storage_client.configuration_row_create = mocker.AsyncMock(return_value=row_configuration) keboola_client.storage_client.configuration_metadata_update = mocker.AsyncMock() name = 'Test Row Configuration' description = 'Test row configuration description' parameters = {'row_param': 'row_value'} storage = {} # Test the create_component_row_configuration tool result = await add_config_row( ctx=context, name=name, description=description, component_id=component_id, configuration_id=configuration_id, parameters=parameters, storage=storage, ) assert isinstance(result, ConfigToolOutput) assert result.component_id == component_id assert result.configuration_id == configuration_id assert result.description == description assert result.success is True assert result.timestamp is not None assert result.version == row_configuration['version'] keboola_client.ai_service_client.get_component_detail.assert_called_once_with(component_id=component_id) keboola_client.storage_client.configuration_row_create.assert_called_once_with( component_id=component_id, config_id=configuration_id, name=name, description=description, configuration={'storage': storage, 'parameters': parameters}, ) @pytest.mark.parametrize( ('parameter_updates', 'storage', 'expected_config'), [ pytest.param( [ ConfigParamSet(op='set', path='api_key', new_val='new_api_key'), ConfigParamReplace(op='str_replace', path='database.host', search_for='old', replace_with='new'), ConfigParamRemove(op='remove', path='deprecated_field'), ], None, { 'parameters': { 'api_key': 'new_api_key', 'database': {'host': 'new_host', 'port': 5432}, 'existing_param': 'existing_value', # 'deprecated_field' is removed }, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, id='parameter_updates_only1', ), pytest.param( [ ConfigParamRemove(op='remove', path='existing_param'), ConfigParamSet(op='set', path='updated_param', new_val='updated_value'), ], None, { 'parameters': { 'api_key': 'old_api_key', 'database': {'host': 'old_host', 'port': 5432}, 'deprecated_field': 'old_value', 'updated_param': 'updated_value', # 'existing_param' is removed }, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, id='parameter_updates_only2', ), pytest.param( [ ConfigParamRemove(op='remove', path='existing_param'), ConfigParamSet(op='set', path='updated_param', new_val='updated_value'), ], {'output': {'tables': []}}, { 'parameters': { 'api_key': 'old_api_key', 'database': {'host': 'old_host', 'port': 5432}, 'deprecated_field': 'old_value', 'updated_param': 'updated_value', # 'existing_param' is removed }, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='both_parameter_updates_and_storage', ), pytest.param( None, {'output': {'tables': []}}, { 'parameters': { 'api_key': 'old_api_key', 'database': {'host': 'old_host', 'port': 5432}, 'deprecated_field': 'old_value', 'existing_param': 'existing_value', }, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='storage_only', ), ], ) @pytest.mark.asyncio async def test_update_config( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], parameter_updates: list[ConfigParamUpdate] | None, storage: dict[str, Any] | None, expected_config: dict[str, Any], ): """Test update_component_root_configuration tool with parameter_updates.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) component_id = mock_component['id'] configuration_id = 'test-config-id' existing_configuration = { 'id': configuration_id, 'name': 'Existing Config', 'description': 'Existing description', 'configuration': { 'parameters': { 'api_key': 'old_api_key', 'database': {'host': 'old_host', 'port': 5432}, 'deprecated_field': 'old_value', 'existing_param': 'existing_value', }, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, 'version': 1, } updated_name = 'Updated Configuration' updated_description = 'Updated configuration description' updated_configuration = { **existing_configuration, 'name': updated_name, 'description': updated_description, 'version': 2, } # Set up the mock for ai_service_client and storage_client keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) keboola_client.storage_client.configuration_detail = mocker.AsyncMock(return_value=existing_configuration) keboola_client.storage_client.configuration_update = mocker.AsyncMock(return_value=updated_configuration) keboola_client.storage_client.configuration_metadata_update = mocker.AsyncMock() change_description = 'Test update with parameter updates' # Test the update_component_root_configuration tool with parameter_updates result = await update_config( ctx=context, name=updated_name, description=updated_description, change_description=change_description, component_id=component_id, configuration_id=configuration_id, parameter_updates=parameter_updates, storage=storage, ) assert isinstance(result, ConfigToolOutput) assert result.component_id == component_id assert result.configuration_id == configuration_id assert result.description == updated_description assert result.success is True assert result.timestamp is not None assert result.version == updated_configuration['version'] keboola_client.ai_service_client.get_component_detail.assert_called_once_with(component_id=component_id) keboola_client.storage_client.configuration_update.assert_called_once_with( component_id=component_id, configuration_id=configuration_id, configuration=expected_config, change_description=change_description, updated_name=updated_name, updated_description=updated_description, ) @pytest.mark.parametrize( ('parameter_updates', 'storage', 'expected_config'), [ pytest.param( [ ConfigParamRemove(op='remove', path='existing_param'), ConfigParamSet(op='set', path='updated_param', new_val='updated_value'), ], {'output': {'tables': []}}, { 'parameters': {'updated_param': 'updated_value'}, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='both_parameter_updates_and_storage', ), pytest.param( [ ConfigParamRemove(op='remove', path='existing_param'), ConfigParamSet(op='set', path='updated_param', new_val='updated_value'), ], None, { 'parameters': {'updated_param': 'updated_value'}, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, id='parameter_updates_only', ), pytest.param( None, {'output': {'tables': []}}, { 'parameters': {'existing_param': 'existing_value'}, 'storage': {'output': {'tables': []}}, 'other_field': 'should_be_preserved', }, id='storage_only', ), ], ) @pytest.mark.asyncio async def test_update_config_row( mocker: MockerFixture, mcp_context_components_configs: Context, mock_component: dict[str, Any], parameter_updates: list[ConfigParamUpdate] | None, storage: dict[str, Any] | None, expected_config: dict[str, Any], ): """Test update_component_row_configuration tool with parameter_updates.""" context = mcp_context_components_configs keboola_client = KeboolaClient.from_state(context.session.state) component_id = mock_component['id'] configuration_id = 'test-config-id' configuration_row_id = 'test-row-id' existing_row_configuration = { 'configuration': { 'parameters': {'existing_param': 'existing_value'}, 'storage': {'input': {'tables': ['existing_table']}}, 'other_field': 'should_be_preserved', }, 'version': 1, } updated_name = 'Updated Row Configuration' updated_description = 'Updated row configuration description' updated_row_configuration = { **existing_row_configuration, 'name': updated_name, 'description': updated_description, 'version': 2, } # Set up the mock for ai_service_client and storage_client keboola_client.ai_service_client = mocker.MagicMock() keboola_client.ai_service_client.get_component_detail = mocker.AsyncMock(return_value=mock_component) keboola_client.storage_client.configuration_row_detail = mocker.AsyncMock(return_value=existing_row_configuration) keboola_client.storage_client.configuration_row_update = mocker.AsyncMock(return_value=updated_row_configuration) keboola_client.storage_client.configuration_metadata_update = mocker.AsyncMock() change_description = 'Test row update' # Test the update_component_row_configuration tool with parameter_updates result = await update_config_row( ctx=context, name=updated_name, description=updated_description, change_description=change_description, component_id=component_id, configuration_id=configuration_id, configuration_row_id=configuration_row_id, parameter_updates=parameter_updates, storage=storage, ) assert isinstance(result, ConfigToolOutput) assert result.component_id == component_id assert result.configuration_id == configuration_id assert result.description == updated_description assert result.success is True assert result.timestamp is not None assert result.version == updated_row_configuration['version'] keboola_client.ai_service_client.get_component_detail.assert_called_once_with(component_id=component_id) keboola_client.storage_client.configuration_row_update.assert_called_once_with( component_id=component_id, config_id=configuration_id, configuration_row_id=configuration_row_id, configuration=expected_config, change_description=change_description, updated_name=updated_name, updated_description=updated_description, )

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keboola/keboola-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server