Skip to content

Commit 58e8f52

Browse files
authored
feat: add workspace tools for list, get, and create operations (#131)
* feat: add workspace tools with list, get, and create functionality * feat: import workspace tools * feat: register workspace tools in tool registry * feat: add comprehensive unit tests for workspace tools * fix: types, format
1 parent 41cb064 commit 58e8f52

3 files changed

Lines changed: 308 additions & 0 deletions

File tree

src/deepset_mcp/tool_factory.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
list_secrets as list_secrets_tool,
5353
)
5454
from deepset_mcp.tools.tokonomics import RichExplorer, explorable, explorable_and_referenceable, referenceable
55+
from deepset_mcp.tools.workspace import (
56+
create_workspace as create_workspace_tool,
57+
get_workspace as get_workspace_tool,
58+
list_workspaces as list_workspaces_tool,
59+
)
5560

5661
EXPLORER = RichExplorer(store=STORE)
5762

@@ -226,6 +231,9 @@ def get_workspace_from_env() -> str:
226231
),
227232
"list_secrets": (list_secrets_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
228233
"get_secret": (get_secret_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
234+
"list_workspaces": (list_workspaces_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
235+
"get_workspace": (get_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
236+
"create_workspace": (create_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
229237
"get_from_object_store": (get_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
230238
"get_slice_from_object_store": (get_slice_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
231239
}

src/deepset_mcp/tools/workspace.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tools for interacting with workspaces."""
2+
3+
from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
4+
from deepset_mcp.api.protocols import AsyncClientProtocol
5+
from deepset_mcp.api.shared_models import NoContentResponse
6+
from deepset_mcp.api.workspace.models import Workspace, WorkspaceList
7+
8+
9+
async def list_workspaces(*, client: AsyncClientProtocol) -> WorkspaceList | str:
10+
"""Retrieves a list of all workspaces available to the user.
11+
12+
This tool provides an overview of all workspaces that the user has access to.
13+
Each workspace contains information about its name, ID, supported languages,
14+
and default idle timeout settings.
15+
16+
:param client: The async client for API communication.
17+
:returns: List of workspaces or error message.
18+
"""
19+
try:
20+
return await client.workspaces().list()
21+
except (BadRequestError, UnexpectedAPIError) as e:
22+
return f"Failed to list workspaces: {e}"
23+
24+
25+
async def get_workspace(*, client: AsyncClientProtocol, workspace_name: str) -> Workspace | str:
26+
"""Fetches detailed information for a specific workspace by name.
27+
28+
This tool retrieves comprehensive details about a specific workspace, including
29+
its unique ID, supported languages, and configuration settings. Use this when
30+
you need detailed information about a particular workspace.
31+
32+
:param client: The async client for API communication.
33+
:param workspace_name: The name of the workspace to fetch details for.
34+
:returns: Workspace details or error message.
35+
"""
36+
try:
37+
return await client.workspaces().get(workspace_name=workspace_name)
38+
except ResourceNotFoundError:
39+
return f"There is no workspace named '{workspace_name}'."
40+
except (BadRequestError, UnexpectedAPIError) as e:
41+
return f"Failed to fetch workspace '{workspace_name}': {e}"
42+
43+
44+
async def create_workspace(*, client: AsyncClientProtocol, name: str) -> NoContentResponse | str:
45+
"""Creates a new workspace with the specified name.
46+
47+
This tool creates a new workspace that can be used to organize pipelines,
48+
indexes, and other resources. The workspace name must be unique across
49+
the platform. Once created, you can start deploying pipelines and other
50+
resources within this workspace.
51+
52+
:param client: The async client for API communication.
53+
:param name: The name for the new workspace. Must be unique.
54+
:returns: Success confirmation or error message.
55+
"""
56+
try:
57+
return await client.workspaces().create(name=name)
58+
except BadRequestError as e:
59+
return f"Failed to create workspace '{name}': {e}"
60+
except UnexpectedAPIError as e:
61+
return f"Failed to create workspace '{name}': {e}"

test/unit/tools/test_workspace.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
from uuid import UUID
2+
3+
import pytest
4+
5+
from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
6+
from deepset_mcp.api.shared_models import NoContentResponse
7+
from deepset_mcp.api.workspace.models import Workspace, WorkspaceList
8+
from deepset_mcp.api.workspace.protocols import WorkspaceResourceProtocol
9+
from deepset_mcp.tools.workspace import create_workspace, get_workspace, list_workspaces
10+
from test.unit.conftest import BaseFakeClient
11+
12+
13+
class FakeWorkspaceResource(WorkspaceResourceProtocol):
14+
"""Fake workspace resource for testing."""
15+
16+
def __init__(
17+
self,
18+
list_response: list[Workspace] | None = None,
19+
get_response: Workspace | None = None,
20+
create_response: NoContentResponse | None = None,
21+
delete_response: NoContentResponse | None = None,
22+
list_exception: Exception | None = None,
23+
get_exception: Exception | None = None,
24+
create_exception: Exception | None = None,
25+
delete_exception: Exception | None = None,
26+
) -> None:
27+
self._list_response = list_response
28+
self._get_response = get_response
29+
self._create_response = create_response
30+
self._delete_response = delete_response
31+
self._list_exception = list_exception
32+
self._get_exception = get_exception
33+
self._create_exception = create_exception
34+
self._delete_exception = delete_exception
35+
36+
async def list(self) -> WorkspaceList:
37+
"""List all workspaces."""
38+
if self._list_exception:
39+
raise self._list_exception
40+
if self._list_response is not None:
41+
return WorkspaceList(data=self._list_response, has_more=False, total=len(self._list_response))
42+
raise NotImplementedError
43+
44+
async def get(self, workspace_name: str) -> Workspace:
45+
"""Get a specific workspace by name."""
46+
if self._get_exception:
47+
raise self._get_exception
48+
if self._get_response is not None:
49+
return self._get_response
50+
raise NotImplementedError
51+
52+
async def create(self, name: str) -> NoContentResponse:
53+
"""Create a new workspace."""
54+
if self._create_exception:
55+
raise self._create_exception
56+
if self._create_response is not None:
57+
return self._create_response
58+
raise NotImplementedError
59+
60+
async def delete(self, workspace_name: str) -> NoContentResponse:
61+
"""Delete a workspace."""
62+
if self._delete_exception:
63+
raise self._delete_exception
64+
if self._delete_response is not None:
65+
return self._delete_response
66+
raise NotImplementedError
67+
68+
69+
class FakeClient(BaseFakeClient):
70+
"""Fake client for testing workspace tools."""
71+
72+
def __init__(self, resource: FakeWorkspaceResource) -> None:
73+
self._resource = resource
74+
super().__init__()
75+
76+
def workspaces(self) -> WorkspaceResourceProtocol:
77+
return self._resource
78+
79+
80+
@pytest.mark.asyncio
81+
async def test_list_workspaces_returns_workspace_list() -> None:
82+
"""Test that list_workspaces returns a list of workspaces."""
83+
workspace1 = Workspace(
84+
name="workspace1",
85+
workspace_id=UUID("11111111-1111-1111-1111-111111111111"),
86+
languages={"en": "English"},
87+
default_idle_timeout_in_seconds=3600,
88+
)
89+
workspace2 = Workspace(
90+
name="workspace2",
91+
workspace_id=UUID("22222222-2222-2222-2222-222222222222"),
92+
languages={"en": "English", "de": "German"},
93+
default_idle_timeout_in_seconds=7200,
94+
)
95+
resource = FakeWorkspaceResource(list_response=[workspace1, workspace2])
96+
client = FakeClient(resource)
97+
98+
result = await list_workspaces(client=client)
99+
100+
assert isinstance(result, WorkspaceList)
101+
assert len(result.data) == 2
102+
assert result.data[0].name == "workspace1"
103+
assert result.data[1].name == "workspace2"
104+
assert result.total == 2
105+
assert result.has_more is False
106+
107+
108+
@pytest.mark.asyncio
109+
async def test_list_workspaces_handles_bad_request_error() -> None:
110+
"""Test that list_workspaces handles BadRequestError."""
111+
resource = FakeWorkspaceResource(list_exception=BadRequestError("Invalid request"))
112+
client = FakeClient(resource)
113+
114+
result = await list_workspaces(client=client)
115+
116+
assert isinstance(result, str)
117+
assert "Failed to list workspaces: Invalid request" in result
118+
119+
120+
@pytest.mark.asyncio
121+
async def test_list_workspaces_handles_unexpected_api_error() -> None:
122+
"""Test that list_workspaces handles UnexpectedAPIError."""
123+
resource = FakeWorkspaceResource(
124+
list_exception=UnexpectedAPIError(status_code=500, message="Internal server error")
125+
)
126+
client = FakeClient(resource)
127+
128+
result = await list_workspaces(client=client)
129+
130+
assert isinstance(result, str)
131+
assert "Failed to list workspaces: Internal server error" in result
132+
133+
134+
@pytest.mark.asyncio
135+
async def test_get_workspace_returns_workspace() -> None:
136+
"""Test that get_workspace returns a workspace object."""
137+
workspace = Workspace(
138+
name="test-workspace",
139+
workspace_id=UUID("12345678-1234-1234-1234-123456789012"),
140+
languages={"en": "English", "fr": "French"},
141+
default_idle_timeout_in_seconds=1800,
142+
)
143+
resource = FakeWorkspaceResource(get_response=workspace)
144+
client = FakeClient(resource)
145+
146+
result = await get_workspace(client=client, workspace_name="test-workspace")
147+
148+
assert isinstance(result, Workspace)
149+
assert result.name == "test-workspace"
150+
assert result.workspace_id == UUID("12345678-1234-1234-1234-123456789012")
151+
assert result.languages == {"en": "English", "fr": "French"}
152+
assert result.default_idle_timeout_in_seconds == 1800
153+
154+
155+
@pytest.mark.asyncio
156+
async def test_get_workspace_handles_resource_not_found() -> None:
157+
"""Test that get_workspace handles ResourceNotFoundError."""
158+
resource = FakeWorkspaceResource(get_exception=ResourceNotFoundError())
159+
client = FakeClient(resource)
160+
161+
result = await get_workspace(client=client, workspace_name="missing-workspace")
162+
163+
assert isinstance(result, str)
164+
assert result == "There is no workspace named 'missing-workspace'."
165+
166+
167+
@pytest.mark.asyncio
168+
async def test_get_workspace_handles_bad_request_error() -> None:
169+
"""Test that get_workspace handles BadRequestError."""
170+
resource = FakeWorkspaceResource(get_exception=BadRequestError("Invalid workspace name"))
171+
client = FakeClient(resource)
172+
173+
result = await get_workspace(client=client, workspace_name="invalid-name")
174+
175+
assert isinstance(result, str)
176+
assert "Failed to fetch workspace 'invalid-name': Invalid workspace name" in result
177+
178+
179+
@pytest.mark.asyncio
180+
async def test_get_workspace_handles_unexpected_api_error() -> None:
181+
"""Test that get_workspace handles UnexpectedAPIError."""
182+
resource = FakeWorkspaceResource(get_exception=UnexpectedAPIError(status_code=503, message="Service unavailable"))
183+
client = FakeClient(resource)
184+
185+
result = await get_workspace(client=client, workspace_name="test-workspace")
186+
187+
assert isinstance(result, str)
188+
assert "Failed to fetch workspace 'test-workspace': Service unavailable" in result
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_create_workspace_returns_success_response() -> None:
193+
"""Test that create_workspace returns a success response."""
194+
success_response = NoContentResponse(message="Workspace created successfully.")
195+
resource = FakeWorkspaceResource(create_response=success_response)
196+
client = FakeClient(resource)
197+
198+
result = await create_workspace(client=client, name="new-workspace")
199+
200+
assert isinstance(result, NoContentResponse)
201+
assert result.message == "Workspace created successfully."
202+
203+
204+
@pytest.mark.asyncio
205+
async def test_create_workspace_handles_bad_request_error() -> None:
206+
"""Test that create_workspace handles BadRequestError."""
207+
resource = FakeWorkspaceResource(create_exception=BadRequestError("Workspace name already exists"))
208+
client = FakeClient(resource)
209+
210+
result = await create_workspace(client=client, name="existing-workspace")
211+
212+
assert isinstance(result, str)
213+
assert "Failed to create workspace 'existing-workspace': Workspace name already exists" in result
214+
215+
216+
@pytest.mark.asyncio
217+
async def test_create_workspace_handles_unexpected_api_error() -> None:
218+
"""Test that create_workspace handles UnexpectedAPIError."""
219+
resource = FakeWorkspaceResource(create_exception=UnexpectedAPIError(status_code=500, message="Database error"))
220+
client = FakeClient(resource)
221+
222+
result = await create_workspace(client=client, name="test-workspace")
223+
224+
assert isinstance(result, str)
225+
assert "Failed to create workspace 'test-workspace': Database error" in result
226+
227+
228+
@pytest.mark.asyncio
229+
async def test_list_workspaces_empty_response() -> None:
230+
"""Test that list_workspaces handles empty workspace list."""
231+
resource = FakeWorkspaceResource(list_response=[])
232+
client = FakeClient(resource)
233+
234+
result = await list_workspaces(client=client)
235+
236+
assert isinstance(result, WorkspaceList)
237+
assert len(result.data) == 0
238+
assert result.total == 0
239+
assert result.has_more is False

0 commit comments

Comments
 (0)