deepset-mcp 0.0.6__py3-none-any.whl → 0.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. deepset_mcp/__init__.py +3 -4
  2. deepset_mcp/api/__init__.py +3 -0
  3. deepset_mcp/api/client.py +126 -107
  4. deepset_mcp/api/custom_components/__init__.py +3 -0
  5. deepset_mcp/api/custom_components/models.py +7 -8
  6. deepset_mcp/api/custom_components/protocols.py +4 -3
  7. deepset_mcp/api/custom_components/resource.py +39 -13
  8. deepset_mcp/api/haystack_service/__init__.py +3 -0
  9. deepset_mcp/api/haystack_service/protocols.py +21 -0
  10. deepset_mcp/api/haystack_service/resource.py +46 -0
  11. deepset_mcp/api/indexes/__init__.py +3 -0
  12. deepset_mcp/api/indexes/models.py +23 -11
  13. deepset_mcp/api/indexes/protocols.py +13 -4
  14. deepset_mcp/api/indexes/resource.py +86 -22
  15. deepset_mcp/api/integrations/__init__.py +4 -0
  16. deepset_mcp/api/integrations/models.py +4 -13
  17. deepset_mcp/api/integrations/protocols.py +3 -3
  18. deepset_mcp/api/integrations/resource.py +5 -5
  19. deepset_mcp/api/pipeline/__init__.py +1 -15
  20. deepset_mcp/api/pipeline/models.py +66 -28
  21. deepset_mcp/api/pipeline/protocols.py +6 -10
  22. deepset_mcp/api/pipeline/resource.py +101 -58
  23. deepset_mcp/api/pipeline_template/__init__.py +3 -0
  24. deepset_mcp/api/pipeline_template/models.py +12 -23
  25. deepset_mcp/api/pipeline_template/protocols.py +11 -5
  26. deepset_mcp/api/pipeline_template/resource.py +51 -39
  27. deepset_mcp/api/protocols.py +13 -11
  28. deepset_mcp/api/secrets/__init__.py +3 -0
  29. deepset_mcp/api/secrets/models.py +2 -8
  30. deepset_mcp/api/secrets/protocols.py +4 -3
  31. deepset_mcp/api/secrets/resource.py +32 -7
  32. deepset_mcp/api/shared_models.py +111 -1
  33. deepset_mcp/api/transport.py +30 -58
  34. deepset_mcp/api/user/__init__.py +3 -0
  35. deepset_mcp/api/workspace/__init__.py +1 -3
  36. deepset_mcp/api/workspace/models.py +4 -8
  37. deepset_mcp/api/workspace/protocols.py +3 -3
  38. deepset_mcp/api/workspace/resource.py +5 -9
  39. deepset_mcp/main.py +5 -20
  40. deepset_mcp/mcp/__init__.py +10 -0
  41. deepset_mcp/{server.py → mcp/server.py} +8 -18
  42. deepset_mcp/{store.py → mcp/store.py} +3 -3
  43. deepset_mcp/{tool_factory.py → mcp/tool_factory.py} +20 -37
  44. deepset_mcp/mcp/tool_models.py +57 -0
  45. deepset_mcp/{tool_registry.py → mcp/tool_registry.py} +16 -6
  46. deepset_mcp/{tools/tokonomics → tokonomics}/__init__.py +3 -1
  47. deepset_mcp/{tools/tokonomics → tokonomics}/decorators.py +2 -2
  48. deepset_mcp/{tools/tokonomics → tokonomics}/explorer.py +1 -1
  49. deepset_mcp/tools/__init__.py +58 -0
  50. deepset_mcp/tools/custom_components.py +7 -4
  51. deepset_mcp/tools/haystack_service.py +64 -22
  52. deepset_mcp/tools/haystack_service_models.py +40 -0
  53. deepset_mcp/tools/indexes.py +131 -32
  54. deepset_mcp/tools/object_store.py +1 -1
  55. deepset_mcp/tools/pipeline.py +40 -10
  56. deepset_mcp/tools/pipeline_template.py +35 -18
  57. deepset_mcp/tools/secrets.py +29 -13
  58. deepset_mcp/tools/workspace.py +2 -2
  59. deepset_mcp-0.0.7.dist-info/METADATA +100 -0
  60. deepset_mcp-0.0.7.dist-info/RECORD +74 -0
  61. deepset_mcp/api/README.md +0 -536
  62. deepset_mcp/api/pipeline/log_level.py +0 -13
  63. deepset_mcp/tool_models.py +0 -42
  64. deepset_mcp-0.0.6.dist-info/METADATA +0 -807
  65. deepset_mcp-0.0.6.dist-info/RECORD +0 -75
  66. /deepset_mcp/{tools/tokonomics → tokonomics}/object_store.py +0 -0
  67. {deepset_mcp-0.0.6.dist-info → deepset_mcp-0.0.7.dist-info}/WHEEL +0 -0
  68. {deepset_mcp-0.0.6.dist-info → deepset_mcp-0.0.7.dist-info}/entry_points.txt +0 -0
  69. {deepset_mcp-0.0.6.dist-info → deepset_mcp-0.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -6,20 +6,20 @@ import json
6
6
  import logging
7
7
  from collections.abc import AsyncIterator
8
8
  from typing import TYPE_CHECKING, Any
9
+ from urllib.parse import quote
9
10
 
10
11
  from deepset_mcp.api.exceptions import UnexpectedAPIError
11
- from deepset_mcp.api.pipeline.log_level import LogLevel
12
12
  from deepset_mcp.api.pipeline.models import (
13
13
  DeepsetPipeline,
14
14
  DeepsetSearchResponse,
15
15
  DeepsetStreamEvent,
16
- PipelineList,
17
- PipelineLogList,
16
+ LogLevel,
17
+ PipelineLog,
18
18
  PipelineValidationResult,
19
19
  ValidationError,
20
20
  )
21
21
  from deepset_mcp.api.pipeline.protocols import PipelineResourceProtocol
22
- from deepset_mcp.api.shared_models import NoContentResponse
22
+ from deepset_mcp.api.shared_models import NoContentResponse, PaginatedResponse
23
23
  from deepset_mcp.api.transport import raise_for_status
24
24
 
25
25
  logger = logging.getLogger(__name__)
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
29
29
 
30
30
 
31
31
  class PipelineResource(PipelineResourceProtocol):
32
- """Manages interactions with the deepset pipeline API."""
32
+ """Interact with pipelines on the deepset AI platform."""
33
33
 
34
34
  def __init__(
35
35
  self,
@@ -54,7 +54,7 @@ class PipelineResource(PipelineResourceProtocol):
54
54
  data = {"query_yaml": yaml_config}
55
55
 
56
56
  resp = await self._client.request(
57
- endpoint=f"v1/workspaces/{self._workspace}/pipeline_validations",
57
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipeline_validations",
58
58
  method="POST",
59
59
  data=data,
60
60
  )
@@ -75,41 +75,45 @@ class PipelineResource(PipelineResourceProtocol):
75
75
 
76
76
  raise UnexpectedAPIError(status_code=resp.status_code, message=resp.text, detail=resp.json)
77
77
 
78
- async def list(
79
- self,
80
- page_number: int = 1,
81
- limit: int = 10,
82
- ) -> PipelineList:
83
- """Retrieve pipeline in the configured workspace with optional pagination.
84
-
85
- :param page_number: Page number for paging.
86
- :param limit: Max number of items to return.
87
- :returns: PipelineList with pipelines and metadata.
78
+ async def list(self, limit: int = 10, after: str | None = None) -> PaginatedResponse[DeepsetPipeline]:
79
+ """Lists pipelines and returns the first page of results.
80
+
81
+ The returned object can be iterated over to fetch subsequent pages.
82
+
83
+ :param limit: The maximum number of pipelines to return per page.
84
+ :param after: The cursor to fetch the next page of results.
85
+ :returns: A `PaginatedResponse` object containing the first page of pipelines.
88
86
  """
89
- params: dict[str, Any] = {
90
- "page_number": page_number,
91
- "limit": limit,
92
- }
87
+ # 1. Prepare arguments for the initial API call
88
+ # TODO: Pagination in the deepset API is currently implemented in an unintuitive way.
89
+ # TODO: The cursor is always time based (created_at) and after signifies pipelines older than the current cursor
90
+ # TODO: while 'before' signals pipelines younger than the current cursor.
91
+ # TODO: This is applied irrespective of any sort (e.g. name) that would conflict with this approach.
92
+ # TODO: Change this to 'after' once the behaviour is fixed on the deepset API
93
+ request_params = {"limit": limit, "before": after}
94
+ request_params = {k: v for k, v in request_params.items() if v is not None}
95
+
96
+ # 2. Make the first API call using a private, stateless method
97
+ page = await self._list_api_call(**request_params)
98
+
99
+ # 3. Inject the logic needed for subsequent fetches into the response object
100
+ page._inject_paginator(
101
+ fetch_func=self._list_api_call,
102
+ # Base args for the *next* fetch don't include initial cursors
103
+ base_args={"limit": limit},
104
+ )
105
+ return page
93
106
 
107
+ async def _list_api_call(self, **kwargs: Any) -> PaginatedResponse[DeepsetPipeline]:
108
+ """A private, stateless method that performs the raw API call."""
94
109
  resp = await self._client.request(
95
- endpoint=f"v1/workspaces/{self._workspace}/pipelines",
96
- method="GET",
97
- params=params,
110
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines", method="GET", params=kwargs
98
111
  )
99
-
100
112
  raise_for_status(resp)
113
+ if resp.json is None:
114
+ raise UnexpectedAPIError(status_code=resp.status_code, message="Empty response", detail=None)
101
115
 
102
- response = resp.json
103
-
104
- if response is not None:
105
- pipelines = [DeepsetPipeline.model_validate(item) for item in response.get("data", [])]
106
- return PipelineList(
107
- data=pipelines,
108
- has_more=response.get("has_more", False),
109
- total=response.get("total", len(pipelines)),
110
- )
111
- else:
112
- return PipelineList(data=[], has_more=False, total=0)
116
+ return PaginatedResponse[DeepsetPipeline].create_with_cursor_field(resp.json, "pipeline_id")
113
117
 
114
118
  async def get(self, pipeline_name: str, include_yaml: bool = True) -> DeepsetPipeline:
115
119
  """Fetch a single pipeline by its name.
@@ -118,14 +122,18 @@ class PipelineResource(PipelineResourceProtocol):
118
122
  :param include_yaml: Whether to include YAML configuration in the response.
119
123
  :returns: DeepsetPipeline instance.
120
124
  """
121
- resp = await self._client.request(endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}")
125
+ resp = await self._client.request(
126
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}"
127
+ )
122
128
  raise_for_status(resp)
123
129
 
124
130
  pipeline = DeepsetPipeline.model_validate(resp.json)
125
131
 
126
132
  if include_yaml:
127
133
  yaml_response = await self._client.request(
128
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/yaml"
134
+ endpoint=(
135
+ f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}/yaml"
136
+ )
129
137
  )
130
138
 
131
139
  raise_for_status(yaml_response)
@@ -135,16 +143,16 @@ class PipelineResource(PipelineResourceProtocol):
135
143
 
136
144
  return pipeline
137
145
 
138
- async def create(self, name: str, yaml_config: str) -> NoContentResponse:
146
+ async def create(self, pipeline_name: str, yaml_config: str) -> NoContentResponse:
139
147
  """Create a new pipeline with a name and YAML config.
140
148
 
141
- :param name: Name of the new pipeline.
149
+ :param pipeline_name: Name of the new pipeline.
142
150
  :param yaml_config: YAML configuration for the pipeline.
143
151
  :returns: NoContentResponse indicating successful creation.
144
152
  """
145
- data = {"name": name, "query_yaml": yaml_config}
153
+ data = {"name": pipeline_name, "query_yaml": yaml_config}
146
154
  resp = await self._client.request(
147
- endpoint=f"v1/workspaces/{self._workspace}/pipelines",
155
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines",
148
156
  method="POST",
149
157
  data=data,
150
158
  )
@@ -170,7 +178,7 @@ class PipelineResource(PipelineResourceProtocol):
170
178
  # Handle name update first if any
171
179
  if updated_pipeline_name is not None:
172
180
  name_resp = await self._client.request(
173
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}",
181
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}",
174
182
  method="PATCH",
175
183
  data={"name": updated_pipeline_name},
176
184
  )
@@ -184,7 +192,9 @@ class PipelineResource(PipelineResourceProtocol):
184
192
 
185
193
  if yaml_config is not None:
186
194
  yaml_resp = await self._client.request(
187
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/yaml",
195
+ endpoint=(
196
+ f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}/yaml"
197
+ ),
188
198
  method="PUT",
189
199
  data={"query_yaml": yaml_config},
190
200
  )
@@ -205,36 +215,62 @@ class PipelineResource(PipelineResourceProtocol):
205
215
  pipeline_name: str,
206
216
  limit: int = 30,
207
217
  level: LogLevel | None = None,
208
- ) -> PipelineLogList:
209
- """Fetch logs for a specific pipeline.
218
+ after: str | None = None,
219
+ ) -> PaginatedResponse[PipelineLog]:
220
+ """Fetch logs for a specific pipeline and returns the first page of results.
221
+
222
+ The returned object can be iterated over to fetch subsequent pages.
210
223
 
211
224
  :param pipeline_name: Name of the pipeline to fetch logs for.
212
- :param limit: Maximum number of log entries to return.
225
+ :param limit: Maximum number of log entries to return per page.
213
226
  :param level: Filter logs by level. If None, returns all levels.
214
- :returns: A PipelineLogList containing the log entries.
227
+ :param after: The cursor to fetch the next page of results.
228
+ :returns: A `PaginatedResponse` object containing the first page of logs.
215
229
  """
216
- params: dict[str, Any] = {
230
+ # 1. Prepare arguments for the initial API call
231
+ request_params = {
217
232
  "limit": limit,
218
233
  "filter": "origin eq 'querypipeline'",
219
234
  }
220
235
 
221
236
  # Add level filter if specified
222
237
  if level is not None:
223
- params["filter"] = f"level eq '{level}' and origin eq 'querypipeline'"
238
+ request_params["filter"] = f"level eq '{level}' and origin eq 'querypipeline'"
239
+
240
+ # Add cursor if provided
241
+ if after is not None:
242
+ request_params["after"] = after
243
+
244
+ # Remove None values
245
+ request_params = {k: v for k, v in request_params.items() if v is not None}
246
+
247
+ # 2. Make the first API call using a private, stateless method
248
+ page = await self._get_logs_api_call(pipeline_name, **request_params)
249
+
250
+ # 3. Inject the logic needed for subsequent fetches into the response object
251
+ page._inject_paginator(
252
+ fetch_func=lambda **kwargs: self._get_logs_api_call(pipeline_name, **kwargs),
253
+ # Base args for the *next* fetch don't include initial cursors
254
+ base_args={"limit": limit, "filter": request_params["filter"]},
255
+ cursor_param="after", # Logs use 'after' cursor, not 'before' like pipelines
256
+ )
257
+ return page
224
258
 
259
+ async def _get_logs_api_call(self, pipeline_name: str, **kwargs: Any) -> PaginatedResponse[PipelineLog]:
260
+ """A private, stateless method that performs the raw API call for logs."""
225
261
  resp = await self._client.request(
226
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/logs",
262
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}/logs",
227
263
  method="GET",
228
- params=params,
264
+ params=kwargs,
229
265
  )
230
266
 
231
267
  raise_for_status(resp)
232
268
 
233
269
  if resp.json is not None:
234
- return PipelineLogList.model_validate(resp.json)
270
+ return PaginatedResponse[PipelineLog].create_with_cursor_field(resp.json, "logged_at")
235
271
  else:
236
- # Return empty log list if no response
237
- return PipelineLogList(data=[], has_more=False, total=0)
272
+ # Return empty paginated response if no JSON data
273
+ return PaginatedResponse[PipelineLog](data=[], has_more=False, total=0)
238
274
 
239
275
  async def deploy(self, pipeline_name: str) -> PipelineValidationResult:
240
276
  """Deploy a pipeline to production.
@@ -244,7 +280,9 @@ class PipelineResource(PipelineResourceProtocol):
244
280
  :raises UnexpectedAPIError: If the API returns an unexpected status code.
245
281
  """
246
282
  resp = await self._client.request(
247
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/deploy",
283
+ endpoint=(
284
+ f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}/deploy"
285
+ ),
248
286
  method="POST",
249
287
  )
250
288
 
@@ -274,7 +312,7 @@ class PipelineResource(PipelineResourceProtocol):
274
312
  :raises UnexpectedAPIError: If the API returns an unexpected status code.
275
313
  """
276
314
  resp = await self._client.request(
277
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}",
315
+ endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}",
278
316
  method="DELETE",
279
317
  )
280
318
 
@@ -315,7 +353,9 @@ class PipelineResource(PipelineResourceProtocol):
315
353
  data["filters"] = filters
316
354
 
317
355
  resp = await self._client.request(
318
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/search",
356
+ endpoint=(
357
+ f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/{quote(pipeline_name, safe='')}/search"
358
+ ),
319
359
  method="POST",
320
360
  data=data,
321
361
  response_type=dict[str, Any],
@@ -365,7 +405,10 @@ class PipelineResource(PipelineResourceProtocol):
365
405
  data["filters"] = filters
366
406
 
367
407
  async with self._client.stream_request(
368
- endpoint=f"v1/workspaces/{self._workspace}/pipelines/{pipeline_name}/search-stream",
408
+ endpoint=(
409
+ f"v1/workspaces/{quote(self._workspace, safe='')}/pipelines/"
410
+ f"{quote(pipeline_name, safe='')}/search-stream"
411
+ ),
369
412
  method="POST",
370
413
  data=data,
371
414
  ) as resp:
@@ -2,3 +2,6 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ from .resource import PipelineTemplateResource
6
+
7
+ __all__ = ["PipelineTemplateResource"]
@@ -20,22 +20,34 @@ class PipelineTemplateTag(BaseModel):
20
20
  """Model representing a tag on a pipeline template."""
21
21
 
22
22
  name: str
23
+ "Human-readable name of the tag"
23
24
  tag_id: UUID
25
+ "Unique identifier for the tag"
24
26
 
25
27
 
26
28
  class PipelineTemplate(BaseModel):
27
29
  """Model representing a pipeline template."""
28
30
 
29
31
  author: str
32
+ "Name of the template author or creator"
30
33
  best_for: list[str]
34
+ "List of use cases this template is best suited for"
31
35
  description: str
36
+ "Detailed description of the pipeline template"
32
37
  template_name: str = Field(alias="pipeline_name")
38
+ "Internal name identifier for the template"
33
39
  display_name: str = Field(alias="name")
40
+ "User-friendly display name for the template"
34
41
  pipeline_template_id: UUID = Field(alias="pipeline_template_id")
42
+ "Unique identifier for the pipeline template"
35
43
  potential_applications: list[str] = Field(alias="potential_applications")
44
+ "List of potential applications and scenarios for this template"
36
45
  yaml_config: str | None = None
46
+ "YAML configuration defining the pipeline structure"
37
47
  tags: list[PipelineTemplateTag]
48
+ "List of tags associated with the template for categorization"
38
49
  pipeline_type: PipelineType
50
+ "Type of pipeline (query or indexing)"
39
51
 
40
52
  @model_validator(mode="before")
41
53
  @classmethod
@@ -62,26 +74,3 @@ class PipelineTemplate(BaseModel):
62
74
  values["yaml_config"] = yaml_config
63
75
 
64
76
  return values
65
-
66
-
67
- class PipelineTemplateList(BaseModel):
68
- """Response model for listing pipeline templates."""
69
-
70
- data: list[PipelineTemplate]
71
- has_more: bool
72
- total: int
73
-
74
-
75
- class PipelineTemplateSearchResult(BaseModel):
76
- """Model representing a search result for pipeline templates."""
77
-
78
- template: PipelineTemplate
79
- similarity_score: float
80
-
81
-
82
- class PipelineTemplateSearchResults(BaseModel):
83
- """Response model for pipeline template search results."""
84
-
85
- results: list[PipelineTemplateSearchResult]
86
- query: str
87
- total_found: int
@@ -4,7 +4,8 @@
4
4
 
5
5
  from typing import Protocol
6
6
 
7
- from deepset_mcp.api.pipeline_template.models import PipelineTemplate, PipelineTemplateList
7
+ from deepset_mcp.api.pipeline_template.models import PipelineTemplate
8
+ from deepset_mcp.api.shared_models import PaginatedResponse
8
9
 
9
10
 
10
11
  class PipelineTemplateResourceProtocol(Protocol):
@@ -14,8 +15,13 @@ class PipelineTemplateResourceProtocol(Protocol):
14
15
  """Fetch a single pipeline template by its name."""
15
16
  ...
16
17
 
17
- async def list_templates(
18
- self, limit: int = 100, field: str = "created_at", order: str = "DESC", filter: str | None = None
19
- ) -> PipelineTemplateList:
20
- """List pipeline templates in the configured workspace."""
18
+ async def list(
19
+ self,
20
+ limit: int = 10,
21
+ after: str | None = None,
22
+ field: str = "created_at",
23
+ order: str = "DESC",
24
+ filter: str | None = None,
25
+ ) -> PaginatedResponse[PipelineTemplate]:
26
+ """Lists pipeline templates and returns the first page of results with pagination support."""
21
27
  ...
@@ -5,9 +5,10 @@
5
5
  from typing import Any
6
6
 
7
7
  from deepset_mcp.api.exceptions import UnexpectedAPIError
8
- from deepset_mcp.api.pipeline_template.models import PipelineTemplate, PipelineTemplateList
8
+ from deepset_mcp.api.pipeline_template.models import PipelineTemplate
9
9
  from deepset_mcp.api.pipeline_template.protocols import PipelineTemplateResourceProtocol
10
10
  from deepset_mcp.api.protocols import AsyncClientProtocol
11
+ from deepset_mcp.api.shared_models import PaginatedResponse
11
12
  from deepset_mcp.api.transport import raise_for_status
12
13
 
13
14
 
@@ -46,47 +47,58 @@ class PipelineTemplateResource(PipelineTemplateResourceProtocol):
46
47
 
47
48
  return PipelineTemplate.model_validate(data)
48
49
 
49
- async def list_templates(
50
- self, limit: int = 100, field: str = "created_at", order: str = "DESC", filter: str | None = None
51
- ) -> PipelineTemplateList:
52
- """List pipeline templates in the configured workspace.
53
-
54
- Parameters
55
- ----------
56
- limit : int, optional (default=100)
57
- Maximum number of templates to return
58
- field : str, optional (default="created_at")
59
- Field to sort by
60
- order : str, optional (default="DESC")
61
- Sort order (ASC or DESC)
62
- filter : str | None, optional (default=None)
63
- OData filter expression for filtering templates
64
-
65
- Returns
66
- -------
67
- PipelineTemplateList
68
- List of pipeline templates with metadata
50
+ async def list(
51
+ self,
52
+ limit: int = 10,
53
+ after: str | None = None,
54
+ field: str = "created_at",
55
+ order: str = "DESC",
56
+ filter: str | None = None,
57
+ ) -> PaginatedResponse[PipelineTemplate]:
58
+ """Lists pipeline templates and returns the first page of results.
59
+
60
+ The returned object can be iterated over to fetch subsequent pages.
61
+
62
+ :param limit: The maximum number of pipeline templates to return per page.
63
+ :param after: The cursor to fetch the next page of results.
64
+ :param field: Field to sort by (default: "created_at").
65
+ :param order: Sort order, either "ASC" or "DESC" (default: "DESC").
66
+ :param filter: OData filter expression for filtering templates.
67
+ :returns: A `PaginatedResponse` object containing the first page of pipeline templates.
69
68
  """
70
- params = {"limit": limit, "page_number": 1, "field": field, "order": order}
71
-
69
+ # TODO: Remove when fixed
70
+ if after is not None:
71
+ raise ValueError("Pagination using 'after' parameter is currently not supported by the deepset platform.")
72
+
73
+ # 1. Prepare arguments for the initial API call
74
+ # TODO: Pagination in the deepset API is currently implemented in an unintuitive way.
75
+ # TODO: The cursor is always time based (created_at) and after signifies templates older than the current cursor
76
+ # TODO: while 'before' signals templates younger than the current cursor.
77
+ # TODO: This is applied irrespective of any sort (e.g. name) that would conflict with this approach.
78
+ # TODO: Change this to 'after' once the behaviour is fixed on the deepset API
79
+ request_params = {"limit": limit, "before": after, "field": field, "order": order}
72
80
  if filter is not None:
73
- params["filter"] = filter
81
+ request_params["filter"] = filter
82
+ request_params = {k: v for k, v in request_params.items() if v is not None}
74
83
 
75
- response = await self._client.request(
76
- f"/v1/workspaces/{self._workspace}/pipeline_templates",
77
- method="GET",
78
- params=params,
79
- )
80
-
81
- raise_for_status(response)
84
+ # 2. Make the first API call using a private, stateless method
85
+ page = await self._list_api_call(**request_params)
82
86
 
83
- if response.json is None:
84
- raise UnexpectedAPIError(message="Unexpected API response, no templates returned.")
85
-
86
- response_data: dict[str, Any] = response.json
87
+ # 3. Inject the logic needed for subsequent fetches into the response object
88
+ page._inject_paginator(
89
+ fetch_func=self._list_api_call,
90
+ # Base args for the *next* fetch don't include initial cursors
91
+ base_args={"limit": limit, "field": field, "order": order, "filter": filter},
92
+ )
93
+ return page
87
94
 
88
- return PipelineTemplateList(
89
- data=[PipelineTemplate.model_validate(template) for template in response_data["data"]],
90
- has_more=response_data.get("has_more", False),
91
- total=response_data.get("total", len(response_data["data"])),
95
+ async def _list_api_call(self, **kwargs: Any) -> PaginatedResponse[PipelineTemplate]:
96
+ """A private, stateless method that performs the raw API call."""
97
+ resp = await self._client.request(
98
+ endpoint=f"v1/workspaces/{self._workspace}/pipeline_templates", method="GET", params=kwargs
92
99
  )
100
+ raise_for_status(resp)
101
+ if resp.json is None:
102
+ raise UnexpectedAPIError(status_code=resp.status_code, message="Empty response", detail=None)
103
+
104
+ return PaginatedResponse[PipelineTemplate].create_with_cursor_field(resp.json, "name")
@@ -4,18 +4,20 @@
4
4
 
5
5
  from contextlib import AbstractAsyncContextManager
6
6
  from types import TracebackType
7
- from typing import Any, Literal, Protocol, Self, TypeVar, overload
8
-
9
- from deepset_mcp.api.custom_components.protocols import CustomComponentsProtocol
10
- from deepset_mcp.api.haystack_service.protocols import HaystackServiceProtocol
11
- from deepset_mcp.api.indexes.protocols import IndexResourceProtocol
12
- from deepset_mcp.api.integrations.protocols import IntegrationResourceProtocol
13
- from deepset_mcp.api.pipeline.protocols import PipelineResourceProtocol
14
- from deepset_mcp.api.pipeline_template.protocols import PipelineTemplateResourceProtocol
15
- from deepset_mcp.api.secrets.protocols import SecretResourceProtocol
7
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, TypeVar, overload
8
+
16
9
  from deepset_mcp.api.transport import StreamingResponse, TransportResponse
17
- from deepset_mcp.api.user.protocols import UserResourceProtocol
18
- from deepset_mcp.api.workspace.protocols import WorkspaceResourceProtocol
10
+
11
+ if TYPE_CHECKING:
12
+ from deepset_mcp.api.custom_components.protocols import CustomComponentsProtocol
13
+ from deepset_mcp.api.haystack_service.protocols import HaystackServiceProtocol
14
+ from deepset_mcp.api.indexes.protocols import IndexResourceProtocol
15
+ from deepset_mcp.api.integrations.protocols import IntegrationResourceProtocol
16
+ from deepset_mcp.api.pipeline.protocols import PipelineResourceProtocol
17
+ from deepset_mcp.api.pipeline_template.protocols import PipelineTemplateResourceProtocol
18
+ from deepset_mcp.api.secrets.protocols import SecretResourceProtocol
19
+ from deepset_mcp.api.user.protocols import UserResourceProtocol
20
+ from deepset_mcp.api.workspace.protocols import WorkspaceResourceProtocol
19
21
 
20
22
  T = TypeVar("T")
21
23
 
@@ -2,3 +2,6 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ from .resource import SecretResource
6
+
7
+ __all__ = ["SecretResource"]
@@ -9,12 +9,6 @@ class Secret(BaseModel):
9
9
  """Model representing a secret in deepset."""
10
10
 
11
11
  name: str
12
+ "Human-readable name of the secret"
12
13
  secret_id: str
13
-
14
-
15
- class SecretList(BaseModel):
16
- """Model representing a list of secrets with pagination."""
17
-
18
- data: list[Secret]
19
- has_more: bool
20
- total: int
14
+ "Unique identifier for the secret"
@@ -4,8 +4,8 @@
4
4
 
5
5
  from typing import Protocol
6
6
 
7
- from deepset_mcp.api.secrets.models import Secret, SecretList
8
- from deepset_mcp.api.shared_models import NoContentResponse
7
+ from deepset_mcp.api.secrets.models import Secret
8
+ from deepset_mcp.api.shared_models import NoContentResponse, PaginatedResponse
9
9
 
10
10
 
11
11
  class SecretResourceProtocol(Protocol):
@@ -16,7 +16,8 @@ class SecretResourceProtocol(Protocol):
16
16
  limit: int = 10,
17
17
  field: str = "created_at",
18
18
  order: str = "DESC",
19
- ) -> SecretList:
19
+ after: str | None = None,
20
+ ) -> PaginatedResponse[Secret]:
20
21
  """List secrets with pagination."""
21
22
  ...
22
23
 
@@ -6,9 +6,9 @@ from typing import Any
6
6
 
7
7
  from deepset_mcp.api.exceptions import ResourceNotFoundError
8
8
  from deepset_mcp.api.protocols import AsyncClientProtocol
9
- from deepset_mcp.api.secrets.models import Secret, SecretList
9
+ from deepset_mcp.api.secrets.models import Secret
10
10
  from deepset_mcp.api.secrets.protocols import SecretResourceProtocol
11
- from deepset_mcp.api.shared_models import NoContentResponse
11
+ from deepset_mcp.api.shared_models import NoContentResponse, PaginatedResponse
12
12
  from deepset_mcp.api.transport import raise_for_status
13
13
 
14
14
 
@@ -27,26 +27,51 @@ class SecretResource(SecretResourceProtocol):
27
27
  limit: int = 10,
28
28
  field: str = "created_at",
29
29
  order: str = "DESC",
30
- ) -> SecretList:
30
+ after: str | None = None,
31
+ ) -> PaginatedResponse[Secret]:
31
32
  """List secrets with pagination.
32
33
 
34
+ The returned object can be iterated over to fetch subsequent pages.
35
+
33
36
  :param limit: Maximum number of secrets to return.
34
37
  :param field: Field to sort by.
35
38
  :param order: Sort order (ASC or DESC).
39
+ :param after: The cursor to fetch the next page of results.
36
40
 
37
- :returns: List of secrets with pagination info.
41
+ :returns: A `PaginatedResponse` object containing the first page of secrets.
38
42
  """
39
- params = {
43
+ # 1. Prepare arguments for the initial API call
44
+ # TODO: Pagination in the deepset API is currently implemented in an unintuitive way.
45
+ # TODO: The cursor is always time based (created_at) and after signifies secrets older than the current cursor
46
+ # TODO: while 'before' signals secrets younger than the current cursor.
47
+ # TODO: This is applied irrespective of any sort (e.g. name) that would conflict with this approach.
48
+ # TODO: Change this to 'after' once the behaviour is fixed on the deepset API
49
+ request_params = {
40
50
  "limit": str(limit),
41
51
  "field": field,
42
52
  "order": order,
53
+ "before": after,
43
54
  }
55
+ request_params = {k: v for k, v in request_params.items() if v is not None}
56
+
57
+ # 2. Make the first API call using a private, stateless method
58
+ page = await self._list_api_call(**request_params)
59
+
60
+ # 3. Inject the logic needed for subsequent fetches into the response object
61
+ page._inject_paginator(
62
+ fetch_func=self._list_api_call,
63
+ # Base args for the *next* fetch don't include initial cursors
64
+ base_args={"limit": str(limit), "field": field, "order": order},
65
+ )
66
+ return page
44
67
 
68
+ async def _list_api_call(self, **kwargs: Any) -> PaginatedResponse[Secret]:
69
+ """A private, stateless method that performs the raw API call."""
45
70
  resp = await self._client.request(
46
71
  endpoint="v2/secrets",
47
72
  method="GET",
48
73
  response_type=dict[str, Any],
49
- params=params,
74
+ params=kwargs,
50
75
  )
51
76
 
52
77
  raise_for_status(resp)
@@ -54,7 +79,7 @@ class SecretResource(SecretResourceProtocol):
54
79
  if resp.json is None:
55
80
  raise ResourceNotFoundError("Failed to retrieve secrets.")
56
81
 
57
- return SecretList(**resp.json)
82
+ return PaginatedResponse[Secret].create_with_cursor_field(resp.json, "secret_id")
58
83
 
59
84
  async def create(self, name: str, secret: str) -> NoContentResponse:
60
85
  """Create a new secret.