Skip to content

Commit 0064fa0

Browse files
authored
Fix issue 26 (#27)
* feat: add get_component_definition function * test: add tests for get_component_definition function * feat: add mcp tool for component definition
1 parent 35e02f3 commit 0064fa0

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

src/deepset_mcp/main.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from mcp.server.fastmcp import FastMCP
44

55
from deepset_mcp.api.client import AsyncDeepsetClient
6-
from deepset_mcp.tools.haystack_service import list_component_families as list_component_families_tool
6+
from deepset_mcp.tools.haystack_service import (
7+
get_component_definition as get_component_definition_tool,
8+
list_component_families as list_component_families_tool,
9+
)
710
from deepset_mcp.tools.pipeline import (
811
create_pipeline as create_pipeline_tool,
912
get_pipeline as get_pipeline_tool,
@@ -100,6 +103,20 @@ async def list_component_families() -> str:
100103
return response
101104

102105

106+
@mcp.tool()
107+
async def get_component_definition(component_type: str) -> str:
108+
"""Use this to get the full definition of a specific component.
109+
110+
The component type is the fully qualified import path of the component class.
111+
For example: haystack.components.converters.xlsx.XLSXToDocument
112+
The component definition contains a description, parameters, and example usage of the component.
113+
"""
114+
async with AsyncDeepsetClient() as client:
115+
response = await get_component_definition_tool(client, component_type)
116+
117+
return response
118+
119+
103120
@mcp.tool()
104121
async def validate_pipeline(yaml_configuration: str) -> str:
105122
"""

src/deepset_mcp/tools/haystack_service.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,62 @@
22
from deepset_mcp.api.protocols import AsyncClientProtocol
33

44

5+
async def get_component_definition(client: AsyncClientProtocol, component_type: str) -> str:
6+
"""Returns the definition of a specific Haystack component.
7+
8+
Args:
9+
client: The API client to use
10+
component_type: Fully qualified component type
11+
(e.g. haystack.components.routers.conditional_router.ConditionalRouter)
12+
13+
Returns:
14+
A formatted string containing the component definition
15+
"""
16+
haystack_service = client.haystack_service()
17+
18+
try:
19+
response = await haystack_service.get_component_schemas()
20+
except UnexpectedAPIError as e:
21+
return f"Failed to retrieve component definition: {e}"
22+
23+
components = response["component_schema"]["definitions"]["Components"]
24+
25+
# Find the component by its type
26+
component_def = None
27+
for comp in components.values():
28+
if comp["properties"]["type"].get("const") == component_type:
29+
component_def = comp
30+
break
31+
32+
if not component_def:
33+
return f"Component not found: {component_type}"
34+
35+
# Extract relevant information
36+
component_type_info = component_def["properties"]["type"]
37+
init_params = component_def["properties"].get("init_parameters", {}).get("properties", {})
38+
39+
# Format the output
40+
parts = [
41+
f"Component: {component_type}",
42+
f"Name: {component_def.get('title', 'Unknown')}",
43+
f"Family: {component_type_info.get('family', 'Unknown')}",
44+
f"Family Description: {component_type_info.get('family_description', 'No description available.')}",
45+
f"\nDescription:\n{component_def.get('description', 'No description available.')}\n",
46+
"\nInitialization Parameters:",
47+
]
48+
49+
if not init_params:
50+
parts.append(" No initialization parameters")
51+
else:
52+
for param_name, param_info in init_params.items():
53+
param_type = param_info.get("_annotation", param_info.get("type", "Unknown"))
54+
param_desc = param_info.get("description", "No description available.")
55+
default = f" (default: {param_info['default']})" if "default" in param_info else ""
56+
parts.append(f" {param_name}: {param_type}{default}\n {param_desc}")
57+
58+
return "\n".join(parts)
59+
60+
561
async def list_component_families(client: AsyncClientProtocol) -> str:
662
"""Lists all Haystack component families that are available on deepset."""
763
haystack_service = client.haystack_service()

test/unit/tools/test_haystack_service.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from deepset_mcp.api.exceptions import UnexpectedAPIError
6-
from deepset_mcp.tools.haystack_service import list_component_families
6+
from deepset_mcp.tools.haystack_service import get_component_definition, list_component_families
77
from test.unit.conftest import BaseFakeClient
88

99

@@ -31,6 +31,76 @@ def haystack_service(self) -> FakeHaystackServiceResource:
3131
return self._resource
3232

3333

34+
@pytest.mark.asyncio
35+
async def test_get_component_definition_success() -> None:
36+
# Sample component definition similar to the example provided
37+
component_type = "haystack.components.converters.xlsx.XLSXToDocument"
38+
response: dict[str, Any] = {
39+
"component_schema": {
40+
"definitions": {
41+
"Components": {
42+
"XLSXToDocument": {
43+
"title": "XLSXToDocument",
44+
"description": "Converts XLSX files into Documents.",
45+
"properties": {
46+
"type": {
47+
"const": component_type,
48+
"family": "converters",
49+
"family_description": "Convert data into a format your pipeline can query.",
50+
},
51+
"init_parameters": {
52+
"properties": {
53+
"sheet_name": {
54+
"_annotation": "typing.Union[str, int, list, None]",
55+
"description": "The name of the sheet to read.",
56+
"default": None,
57+
},
58+
"table_format": {
59+
"_annotation": "str",
60+
"description": "The format to convert the Excel file to.",
61+
"default": "csv",
62+
},
63+
}
64+
},
65+
},
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
resource = FakeHaystackServiceResource(get_component_schemas_response=response)
73+
client = FakeClient(resource)
74+
result = await get_component_definition(client, component_type)
75+
76+
# Check that all required information is present
77+
assert component_type in result
78+
assert "XLSXToDocument" in result
79+
assert "converters" in result
80+
assert "Convert data into a format" in result
81+
assert "sheet_name" in result
82+
assert "table_format" in result
83+
assert "default: csv" in result
84+
85+
86+
@pytest.mark.asyncio
87+
async def test_get_component_definition_not_found() -> None:
88+
response: dict[str, Any] = {"component_schema": {"definitions": {"Components": {}}}}
89+
resource = FakeHaystackServiceResource(get_component_schemas_response=response)
90+
client = FakeClient(resource)
91+
result = await get_component_definition(client, "nonexistent.component")
92+
assert "Component not found" in result
93+
94+
95+
@pytest.mark.asyncio
96+
async def test_get_component_definition_api_error() -> None:
97+
resource = FakeHaystackServiceResource(exception=UnexpectedAPIError(status_code=500, message="API Error"))
98+
client = FakeClient(resource)
99+
result = await get_component_definition(client, "some.component")
100+
assert "Failed to retrieve component definition" in result
101+
assert "API Error" in result
102+
103+
34104
@pytest.mark.asyncio
35105
async def test_list_component_families_no_families() -> None:
36106
response: dict[str, Any] = {"component_schema": {"definitions": {"Components": {}}}}

0 commit comments

Comments
 (0)