Skip to content

Commit c02b113

Browse files
authored
feat: add skip_validation_errors parameter to create_pipeline and update_pipeline (#90)
* feat: add skip_validation_errors parameter to create_pipeline * feat: add skip_validation_errors parameter to update_pipeline * feat: add skip_validation_errors parameter to create_pipeline MCP tool * feat: add skip_validation_errors parameter to update_pipeline MCP tool * test: update create_pipeline test to use skip_validation_errors=False * test: update update_pipeline test to use skip_validation_errors=False * test: add tests for skip_validation_errors functionality * test: add tests for skip_validation_errors parameter in create_pipeline * test: add tests for skip_validation_errors parameter in update_pipeline * fix: linting, remove validation param from mcp * fix: format
1 parent 786322b commit c02b113

3 files changed

Lines changed: 152 additions & 10 deletions

File tree

src/deepset_mcp/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,12 @@ async def get_pipeline(pipeline_name: str) -> str:
121121

122122
@mcp.tool()
123123
async def create_pipeline(pipeline_name: str, yaml_configuration: str) -> str:
124-
"""Creates a new pipeline in deepset."""
124+
"""Creates a new pipeline in deepset.
125+
126+
Args:
127+
pipeline_name: Name of the pipeline to create
128+
yaml_configuration: YAML configuration for the pipeline
129+
"""
125130
workspace = get_workspace()
126131
async with AsyncDeepsetClient() as client:
127132
response = await create_pipeline_tool(client, workspace, pipeline_name, yaml_configuration)
@@ -138,6 +143,11 @@ async def update_pipeline(
138143
The update is performed by replacing the original configuration snippet with the new one.
139144
Make sure that your original snippet only has a single exact match in the pipeline configuration.
140145
Respect whitespace and formatting.
146+
147+
Args:
148+
pipeline_name: Name of the pipeline to update
149+
original_configuration_snippet: The configuration snippet to replace
150+
replacement_configuration_snippet: The new configuration snippet
141151
"""
142152
workspace = get_workspace()
143153
async with AsyncDeepsetClient() as client:

src/deepset_mcp/tools/pipeline.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,25 @@ async def validate_pipeline(client: AsyncClientProtocol, workspace: str, yaml_co
3636

3737

3838
async def create_pipeline(
39-
client: AsyncClientProtocol, workspace: str, pipeline_name: str, yaml_configuration: str
39+
client: AsyncClientProtocol,
40+
workspace: str,
41+
pipeline_name: str,
42+
yaml_configuration: str,
43+
skip_validation_errors: bool = True,
4044
) -> str:
41-
"""Creates a new pipeline within the currently configured deepset workspace."""
45+
"""Creates a new pipeline within the currently configured deepset workspace.
46+
47+
Args:
48+
client: The async client for API communication
49+
workspace: The workspace name
50+
pipeline_name: Name of the pipeline to create
51+
yaml_configuration: YAML configuration for the pipeline
52+
skip_validation_errors: If True (default), creates the pipeline even if validation fails.
53+
If False, stops creation when validation fails.
54+
"""
4255
validation_response = await client.pipelines(workspace=workspace).validate(yaml_configuration)
4356

44-
if not validation_response.valid:
57+
if not validation_response.valid and not skip_validation_errors:
4558
return validation_result_to_llm_readable_string(validation_response)
4659

4760
try:
@@ -53,7 +66,14 @@ async def create_pipeline(
5366
except UnexpectedAPIError as e:
5467
return f"Failed to create pipeline '{pipeline_name}': {e}"
5568

56-
return f"Pipeline '{pipeline_name}' created successfully."
69+
success_message = f"Pipeline '{pipeline_name}' created successfully."
70+
71+
# If validation failed, but we created anyway, include validation errors
72+
if not validation_response.valid:
73+
validation_errors = validation_result_to_llm_readable_string(validation_response)
74+
return f"{success_message}\n\n**Note: Pipeline was created despite validation issues:**\n{validation_errors}"
75+
76+
return success_message
5777

5878

5979
async def update_pipeline(
@@ -62,13 +82,23 @@ async def update_pipeline(
6282
pipeline_name: str,
6383
original_config_snippet: str,
6484
replacement_config_snippet: str,
85+
skip_validation_errors: bool = True,
6586
) -> str:
6687
"""
6788
Updates a pipeline configuration in the specified workspace with a replacement configuration snippet.
6889
6990
This function validates the replacement configuration snippet before applying it to the pipeline.
70-
If the validation fails, it returns a readable string describing validation errors. Otherwise, the
71-
replacement snippet is used to update the pipeline's configuration in the target workspace.
91+
If the validation fails and skip_validation_errors is False, it returns a readable string describing
92+
validation errors. Otherwise, the replacement snippet is used to update the pipeline's configuration.
93+
94+
Args:
95+
client: The async client for API communication
96+
workspace: The workspace name
97+
pipeline_name: Name of the pipeline to update
98+
original_config_snippet: The configuration snippet to replace
99+
replacement_config_snippet: The new configuration snippet
100+
skip_validation_errors: If True (default), updates the pipeline even if validation fails.
101+
If False, stops update when validation fails.
72102
"""
73103
try:
74104
original_pipeline = await client.pipelines(workspace=workspace).get(pipeline_name=pipeline_name)
@@ -95,7 +125,7 @@ async def update_pipeline(
95125

96126
validation_response = await client.pipelines(workspace=workspace).validate(updated_yaml_configuration)
97127

98-
if not validation_response.valid:
128+
if not validation_response.valid and not skip_validation_errors:
99129
return validation_result_to_llm_readable_string(validation_response)
100130

101131
try:
@@ -109,7 +139,14 @@ async def update_pipeline(
109139
except UnexpectedAPIError as e:
110140
return f"Failed to update the pipeline '{pipeline_name}': {e}"
111141

112-
return f"The pipeline '{pipeline_name}' was successfully updated."
142+
success_message = f"The pipeline '{pipeline_name}' was successfully updated."
143+
144+
# If validation failed, but we updated anyway, include validation errors
145+
if not validation_response.valid:
146+
validation_errors = validation_result_to_llm_readable_string(validation_response)
147+
return f"{success_message}\n\n**Note: Pipeline was updated despite validation issues:**\n{validation_errors}"
148+
149+
return success_message
113150

114151

115152
async def get_pipeline_logs(

test/unit/tools/test_pipeline.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ async def test_create_pipeline_handles_validation_failure() -> None:
223223
invalid_result = PipelineValidationResult(valid=False, errors=[ValidationError(code="E", message="Err")])
224224
resource = FakePipelineResource(validate_response=invalid_result)
225225
client = FakeClient(resource)
226-
result = await create_pipeline(client, workspace="ws", pipeline_name="pname", yaml_configuration="cfg")
226+
result = await create_pipeline(
227+
client, workspace="ws", pipeline_name="pname", yaml_configuration="cfg", skip_validation_errors=False
228+
)
227229
assert "invalid" in result.lower()
228230
assert "Error 1" in result
229231

@@ -251,6 +253,42 @@ async def test_create_pipeline_handles_success_and_failure_response() -> None:
251253
assert "Failed to create pipeline 'p1': bad things (Status Code: 400)" == res_fail
252254

253255

256+
@pytest.mark.asyncio
257+
async def test_create_pipeline_skip_validation_errors_true() -> None:
258+
"""Test that create_pipeline creates the pipeline despite validation errors."""
259+
invalid_result = PipelineValidationResult(
260+
valid=False, errors=[ValidationError(code="E1", message="Test error message")]
261+
)
262+
resource = FakePipelineResource(
263+
validate_response=invalid_result,
264+
create_response=NoContentResponse(message="created successfully"),
265+
)
266+
client = FakeClient(resource)
267+
268+
# Test with explicit True
269+
result = await create_pipeline(
270+
client,
271+
workspace="ws",
272+
pipeline_name="test_pipeline",
273+
yaml_configuration="config: test",
274+
skip_validation_errors=True,
275+
)
276+
277+
assert "Pipeline 'test_pipeline' created successfully." in result
278+
assert "Note: Pipeline was created despite validation issues:" in result
279+
assert "configuration is invalid" in result
280+
assert "Error 1" in result
281+
assert "Test error message" in result
282+
283+
# Test with default (should behave the same as True)
284+
result_default = await create_pipeline(
285+
client, workspace="ws", pipeline_name="test_pipeline", yaml_configuration="config: test"
286+
)
287+
288+
assert "Pipeline 'test_pipeline' created successfully." in result_default
289+
assert "Note: Pipeline was created despite validation issues:" in result_default
290+
291+
254292
@pytest.mark.asyncio
255293
async def test_update_pipeline_not_found_on_get() -> None:
256294
resource = FakePipelineResource(get_exception=ResourceNotFoundError())
@@ -330,6 +368,7 @@ async def test_update_pipeline_validation_failure() -> None:
330368
pipeline_name="np",
331369
original_config_snippet="foo: 1",
332370
replacement_config_snippet="foo: 2",
371+
skip_validation_errors=False,
333372
)
334373
assert "invalid" in res.lower()
335374
assert "Error 1" in res
@@ -430,6 +469,62 @@ async def test_update_pipeline_success_response() -> None:
430469
assert "successfully updated" in r_success.lower()
431470

432471

472+
@pytest.mark.asyncio
473+
async def test_update_pipeline_skip_validation_errors_true() -> None:
474+
"""Test that update_pipeline updates the pipeline despite validation errors."""
475+
user = DeepsetUser(user_id="u1", given_name="A", family_name="B")
476+
orig_yaml = "foo: 1"
477+
original = DeepsetPipeline(
478+
pipeline_id="p",
479+
name="np",
480+
status="S",
481+
service_level=PipelineServiceLevel.DEVELOPMENT,
482+
created_at=datetime.now(),
483+
last_edited_at=None,
484+
created_by=user,
485+
last_edited_by=None,
486+
yaml_config=orig_yaml,
487+
)
488+
invalid_result = PipelineValidationResult(
489+
valid=False, errors=[ValidationError(code="E1", message="Test error message")]
490+
)
491+
492+
resource = FakePipelineResource(
493+
get_response=original,
494+
validate_response=invalid_result,
495+
update_response=NoContentResponse(message="successfully updated"),
496+
)
497+
client = FakeClient(resource)
498+
499+
# Test with explicit True
500+
result = await update_pipeline(
501+
client,
502+
workspace="ws",
503+
pipeline_name="np",
504+
original_config_snippet="foo: 1",
505+
replacement_config_snippet="foo: 2",
506+
skip_validation_errors=True,
507+
)
508+
509+
assert "The pipeline 'np' was successfully updated." in result
510+
assert "Note: Pipeline was updated despite validation issues:" in result
511+
assert "configuration is invalid" in result
512+
assert "Error 1" in result
513+
assert "Test error message" in result
514+
515+
# Test with default (should behave the same as True)
516+
result_default = await update_pipeline(
517+
client,
518+
workspace="ws",
519+
pipeline_name="np",
520+
original_config_snippet="foo: 1",
521+
replacement_config_snippet="foo: 2",
522+
)
523+
524+
assert "The pipeline 'np' was successfully updated." in result_default
525+
assert "Note: Pipeline was updated despite validation issues:" in result_default
526+
527+
433528
@pytest.mark.asyncio
434529
async def test_get_pipeline_logs_success() -> None:
435530
log1 = PipelineLog(

0 commit comments

Comments
 (0)