deepset-mcp 0.0.2rc1__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 (114) hide show
  1. deepset_mcp/__init__.py +0 -0
  2. deepset_mcp/agents/__init__.py +0 -0
  3. deepset_mcp/agents/debugging/__init__.py +0 -0
  4. deepset_mcp/agents/debugging/debugging_agent.py +37 -0
  5. deepset_mcp/agents/debugging/system_prompt.md +214 -0
  6. deepset_mcp/agents/generalist/__init__.py +0 -0
  7. deepset_mcp/agents/generalist/generalist_agent.py +38 -0
  8. deepset_mcp/agents/generalist/system_prompt.md +241 -0
  9. deepset_mcp/api/README.md +536 -0
  10. deepset_mcp/api/__init__.py +0 -0
  11. deepset_mcp/api/client.py +277 -0
  12. deepset_mcp/api/custom_components/__init__.py +0 -0
  13. deepset_mcp/api/custom_components/models.py +25 -0
  14. deepset_mcp/api/custom_components/protocols.py +17 -0
  15. deepset_mcp/api/custom_components/resource.py +56 -0
  16. deepset_mcp/api/exceptions.py +70 -0
  17. deepset_mcp/api/haystack_service/__init__.py +0 -0
  18. deepset_mcp/api/haystack_service/protocols.py +13 -0
  19. deepset_mcp/api/haystack_service/resource.py +55 -0
  20. deepset_mcp/api/indexes/__init__.py +0 -0
  21. deepset_mcp/api/indexes/models.py +63 -0
  22. deepset_mcp/api/indexes/protocols.py +53 -0
  23. deepset_mcp/api/indexes/resource.py +138 -0
  24. deepset_mcp/api/integrations/__init__.py +1 -0
  25. deepset_mcp/api/integrations/models.py +49 -0
  26. deepset_mcp/api/integrations/protocols.py +27 -0
  27. deepset_mcp/api/integrations/resource.py +57 -0
  28. deepset_mcp/api/pipeline/__init__.py +17 -0
  29. deepset_mcp/api/pipeline/log_level.py +9 -0
  30. deepset_mcp/api/pipeline/models.py +235 -0
  31. deepset_mcp/api/pipeline/protocols.py +83 -0
  32. deepset_mcp/api/pipeline/resource.py +378 -0
  33. deepset_mcp/api/pipeline_template/__init__.py +0 -0
  34. deepset_mcp/api/pipeline_template/models.py +56 -0
  35. deepset_mcp/api/pipeline_template/protocols.py +17 -0
  36. deepset_mcp/api/pipeline_template/resource.py +88 -0
  37. deepset_mcp/api/protocols.py +122 -0
  38. deepset_mcp/api/secrets/__init__.py +0 -0
  39. deepset_mcp/api/secrets/models.py +16 -0
  40. deepset_mcp/api/secrets/protocols.py +29 -0
  41. deepset_mcp/api/secrets/resource.py +112 -0
  42. deepset_mcp/api/shared_models.py +17 -0
  43. deepset_mcp/api/transport.py +336 -0
  44. deepset_mcp/api/user/__init__.py +0 -0
  45. deepset_mcp/api/user/protocols.py +11 -0
  46. deepset_mcp/api/user/resource.py +38 -0
  47. deepset_mcp/api/workspace/__init__.py +7 -0
  48. deepset_mcp/api/workspace/models.py +23 -0
  49. deepset_mcp/api/workspace/protocols.py +41 -0
  50. deepset_mcp/api/workspace/resource.py +94 -0
  51. deepset_mcp/benchmark/README.md +425 -0
  52. deepset_mcp/benchmark/__init__.py +1 -0
  53. deepset_mcp/benchmark/agent_configs/debugging_agent.yml +10 -0
  54. deepset_mcp/benchmark/agent_configs/generalist_agent.yml +6 -0
  55. deepset_mcp/benchmark/dp_validation_error_analysis/__init__.py +0 -0
  56. deepset_mcp/benchmark/dp_validation_error_analysis/eda.ipynb +757 -0
  57. deepset_mcp/benchmark/dp_validation_error_analysis/prepare_interaction_data.ipynb +167 -0
  58. deepset_mcp/benchmark/dp_validation_error_analysis/preprocessing_utils.py +213 -0
  59. deepset_mcp/benchmark/runner/__init__.py +0 -0
  60. deepset_mcp/benchmark/runner/agent_benchmark_runner.py +561 -0
  61. deepset_mcp/benchmark/runner/agent_loader.py +110 -0
  62. deepset_mcp/benchmark/runner/cli.py +39 -0
  63. deepset_mcp/benchmark/runner/cli_agent.py +373 -0
  64. deepset_mcp/benchmark/runner/cli_index.py +71 -0
  65. deepset_mcp/benchmark/runner/cli_pipeline.py +73 -0
  66. deepset_mcp/benchmark/runner/cli_tests.py +226 -0
  67. deepset_mcp/benchmark/runner/cli_utils.py +61 -0
  68. deepset_mcp/benchmark/runner/config.py +73 -0
  69. deepset_mcp/benchmark/runner/config_loader.py +64 -0
  70. deepset_mcp/benchmark/runner/interactive.py +140 -0
  71. deepset_mcp/benchmark/runner/models.py +203 -0
  72. deepset_mcp/benchmark/runner/repl.py +67 -0
  73. deepset_mcp/benchmark/runner/setup_actions.py +238 -0
  74. deepset_mcp/benchmark/runner/streaming.py +360 -0
  75. deepset_mcp/benchmark/runner/teardown_actions.py +196 -0
  76. deepset_mcp/benchmark/runner/tracing.py +21 -0
  77. deepset_mcp/benchmark/tasks/chat_rag_answers_wrong_format.yml +16 -0
  78. deepset_mcp/benchmark/tasks/documents_output_wrong.yml +13 -0
  79. deepset_mcp/benchmark/tasks/jinja_str_instead_of_complex_type.yml +11 -0
  80. deepset_mcp/benchmark/tasks/jinja_syntax_error.yml +11 -0
  81. deepset_mcp/benchmark/tasks/missing_output_mapping.yml +14 -0
  82. deepset_mcp/benchmark/tasks/no_query_input.yml +13 -0
  83. deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_str.yml +141 -0
  84. deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_syntax.yml +141 -0
  85. deepset_mcp/benchmark/tasks/pipelines/chat_rag_answers_wrong_format.yml +181 -0
  86. deepset_mcp/benchmark/tasks/pipelines/chat_rag_missing_output_mapping.yml +189 -0
  87. deepset_mcp/benchmark/tasks/pipelines/rag_documents_wrong_format.yml +193 -0
  88. deepset_mcp/benchmark/tasks/pipelines/rag_no_query_input.yml +191 -0
  89. deepset_mcp/benchmark/tasks/pipelines/standard_index.yml +167 -0
  90. deepset_mcp/initialize_embedding_model.py +12 -0
  91. deepset_mcp/main.py +133 -0
  92. deepset_mcp/prompts/deepset_copilot_prompt.md +271 -0
  93. deepset_mcp/prompts/deepset_debugging_agent.md +214 -0
  94. deepset_mcp/store.py +5 -0
  95. deepset_mcp/tool_factory.py +473 -0
  96. deepset_mcp/tools/__init__.py +0 -0
  97. deepset_mcp/tools/custom_components.py +52 -0
  98. deepset_mcp/tools/doc_search.py +83 -0
  99. deepset_mcp/tools/haystack_service.py +358 -0
  100. deepset_mcp/tools/haystack_service_models.py +97 -0
  101. deepset_mcp/tools/indexes.py +129 -0
  102. deepset_mcp/tools/model_protocol.py +16 -0
  103. deepset_mcp/tools/pipeline.py +335 -0
  104. deepset_mcp/tools/pipeline_template.py +116 -0
  105. deepset_mcp/tools/secrets.py +45 -0
  106. deepset_mcp/tools/tokonomics/__init__.py +73 -0
  107. deepset_mcp/tools/tokonomics/decorators.py +396 -0
  108. deepset_mcp/tools/tokonomics/explorer.py +347 -0
  109. deepset_mcp/tools/tokonomics/object_store.py +177 -0
  110. deepset_mcp/tools/workspace.py +61 -0
  111. deepset_mcp-0.0.2rc1.dist-info/METADATA +292 -0
  112. deepset_mcp-0.0.2rc1.dist-info/RECORD +114 -0
  113. deepset_mcp-0.0.2rc1.dist-info/WHEEL +4 -0
  114. deepset_mcp-0.0.2rc1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,473 @@
1
+ """Factory for creating workspace-aware MCP tools."""
2
+
3
+ import functools
4
+ import inspect
5
+ import logging
6
+ import os
7
+ from collections.abc import Awaitable, Callable
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum
10
+ from typing import Any
11
+
12
+ from mcp.server.fastmcp import FastMCP
13
+
14
+ from deepset_mcp.api.client import AsyncDeepsetClient
15
+ from deepset_mcp.initialize_embedding_model import get_initialized_model
16
+ from deepset_mcp.store import STORE
17
+ from deepset_mcp.tools.custom_components import (
18
+ get_latest_custom_component_installation_logs as get_latest_custom_component_installation_logs_tool,
19
+ list_custom_component_installations as list_custom_component_installations_tool,
20
+ )
21
+ from deepset_mcp.tools.doc_search import (
22
+ get_docs_config,
23
+ search_docs as search_docs_tool,
24
+ )
25
+ from deepset_mcp.tools.haystack_service import (
26
+ get_component_definition as get_component_definition_tool,
27
+ get_custom_components as get_custom_components_tool,
28
+ list_component_families as list_component_families_tool,
29
+ search_component_definition as search_component_definition_tool,
30
+ )
31
+ from deepset_mcp.tools.indexes import (
32
+ create_index as create_index_tool,
33
+ deploy_index as deploy_index_tool,
34
+ get_index as get_index_tool,
35
+ list_indexes as list_indexes_tool,
36
+ update_index as update_index_tool,
37
+ )
38
+
39
+ # Import all tool functions
40
+ from deepset_mcp.tools.pipeline import (
41
+ create_pipeline as create_pipeline_tool,
42
+ deploy_pipeline as deploy_pipeline_tool,
43
+ get_pipeline as get_pipeline_tool,
44
+ get_pipeline_logs as get_pipeline_logs_tool,
45
+ list_pipelines as list_pipelines_tool,
46
+ search_pipeline as search_pipeline_tool,
47
+ update_pipeline as update_pipeline_tool,
48
+ validate_pipeline as validate_pipeline_tool,
49
+ )
50
+ from deepset_mcp.tools.pipeline_template import (
51
+ get_pipeline_template as get_pipeline_template_tool,
52
+ list_pipeline_templates as list_pipeline_templates_tool,
53
+ search_pipeline_templates as search_pipeline_templates_tool,
54
+ )
55
+ from deepset_mcp.tools.secrets import (
56
+ get_secret as get_secret_tool,
57
+ list_secrets as list_secrets_tool,
58
+ )
59
+ from deepset_mcp.tools.tokonomics import RichExplorer, explorable, explorable_and_referenceable, referenceable
60
+ from deepset_mcp.tools.workspace import (
61
+ create_workspace as create_workspace_tool,
62
+ get_workspace as get_workspace_tool,
63
+ list_workspaces as list_workspaces_tool,
64
+ )
65
+
66
+ EXPLORER = RichExplorer(store=STORE)
67
+
68
+
69
+ def get_from_object_store(object_id: str, path: str = "") -> str:
70
+ """Use this tool to fetch an object from the object store.
71
+
72
+ You can fetch a specific object by using the object's id (e.g. `@obj_001`).
73
+ You can also fetch any nested path by using the path-parameter
74
+ (e.g. `{"object_id": "@obj_001", "path": "user_info.given_name"}`
75
+ -> returns the content at obj.user_info.given_name).
76
+
77
+ :param object_id: The id of the object to fetch in the format `@obj_001`.
78
+ :param path: The path of the object to fetch in the format of `access.to.attr` or `["access"]["to"]["attr"]`.
79
+ """
80
+ return EXPLORER.explore(obj_id=object_id, path=path)
81
+
82
+
83
+ def get_slice_from_object_store(
84
+ object_id: str,
85
+ start: int = 0,
86
+ end: int | None = None,
87
+ path: str = "",
88
+ ) -> str:
89
+ """Extract a slice from a string or list object that is stored in the object store.
90
+
91
+ :param object_id: Identifier of the object.
92
+ :param start: Start index for slicing.
93
+ :param end: End index for slicing (optional - leave empty to get slice from start to end of sequence).
94
+ :param path: Navigation path to object to slice (optional).
95
+ :return: String representation of the slice.
96
+ """
97
+ return EXPLORER.slice(obj_id=object_id, start=start, end=end, path=path)
98
+
99
+
100
+ async def search_docs(query: str) -> str:
101
+ """Search the deepset platform documentation.
102
+
103
+ This tool allows you to search through deepset's official documentation to find
104
+ information about features, API usage, best practices, and troubleshooting guides.
105
+ Use this when you need to look up specific deepset functionality or help users
106
+ understand how to use deepset features.
107
+
108
+ :param query: The search query to execute against the documentation.
109
+ :returns: The formatted search results from the documentation.
110
+ """
111
+ docs_config = get_docs_config()
112
+ if not docs_config:
113
+ raise RuntimeError("Documentation search configuration not available")
114
+
115
+ docs_workspace, docs_pipeline_name, docs_api_key = docs_config
116
+ async with AsyncDeepsetClient(api_key=docs_api_key) as client:
117
+ response = await search_docs_tool(
118
+ client=client,
119
+ workspace=docs_workspace,
120
+ pipeline_name=docs_pipeline_name,
121
+ query=query,
122
+ )
123
+ return response
124
+
125
+
126
+ class WorkspaceMode(StrEnum):
127
+ """Configuration for how workspace is provided to tools."""
128
+
129
+ IMPLICIT = "implicit" # workspace from env, no parameter in tool signature
130
+ EXPLICIT = "explicit" # workspace as required parameter in tool signature
131
+
132
+
133
+ class MemoryType(StrEnum):
134
+ """Configuration for how memory is provided to tools."""
135
+
136
+ EXPLORABLE = "explorable"
137
+ REFERENCEABLE = "referenceable"
138
+ BOTH = "both"
139
+ NO_MEMORY = "no_memory"
140
+
141
+
142
+ @dataclass
143
+ class ToolConfig:
144
+ """Configuration for tool registration."""
145
+
146
+ needs_client: bool = False
147
+ needs_workspace: bool = False
148
+ memory_type: MemoryType = MemoryType.NO_MEMORY
149
+ custom_args: dict[str, Any] | None = None # For special cases like search_component_definition
150
+
151
+
152
+ def get_workspace_from_env() -> str:
153
+ """Gets the workspace configured from environment variable."""
154
+ workspace = os.environ.get("DEEPSET_WORKSPACE")
155
+ if not workspace:
156
+ raise ValueError("DEEPSET_WORKSPACE environment variable not set")
157
+ return workspace
158
+
159
+
160
+ TOOL_REGISTRY: dict[str, tuple[Callable[..., Any], ToolConfig]] = {
161
+ # Workspace tools
162
+ "list_pipelines": (
163
+ list_pipelines_tool,
164
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
165
+ ),
166
+ "create_pipeline": (
167
+ create_pipeline_tool,
168
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
169
+ ),
170
+ "update_pipeline": (
171
+ update_pipeline_tool,
172
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
173
+ ),
174
+ "get_pipeline": (
175
+ get_pipeline_tool,
176
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
177
+ ),
178
+ "deploy_pipeline": (
179
+ deploy_pipeline_tool,
180
+ ToolConfig(
181
+ needs_client=True,
182
+ needs_workspace=True,
183
+ memory_type=MemoryType.EXPLORABLE,
184
+ custom_args={"wait_for_deployment": True, "timeout_seconds": 600, "poll_interval": 5},
185
+ ),
186
+ ),
187
+ "validate_pipeline": (
188
+ validate_pipeline_tool,
189
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
190
+ ),
191
+ "get_pipeline_logs": (
192
+ get_pipeline_logs_tool,
193
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
194
+ ),
195
+ "search_pipeline": (
196
+ search_pipeline_tool,
197
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
198
+ ),
199
+ "list_indexes": (
200
+ list_indexes_tool,
201
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
202
+ ),
203
+ "get_index": (
204
+ get_index_tool,
205
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
206
+ ),
207
+ "create_index": (
208
+ create_index_tool,
209
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
210
+ ),
211
+ "update_index": (
212
+ update_index_tool,
213
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
214
+ ),
215
+ "deploy_index": (
216
+ deploy_index_tool,
217
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
218
+ ),
219
+ "list_pipeline_templates": (
220
+ list_pipeline_templates_tool,
221
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
222
+ ),
223
+ "get_pipeline_template": (
224
+ get_pipeline_template_tool,
225
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
226
+ ),
227
+ "search_pipeline_templates": (
228
+ search_pipeline_templates_tool,
229
+ ToolConfig(
230
+ needs_client=True,
231
+ needs_workspace=True,
232
+ memory_type=MemoryType.EXPLORABLE,
233
+ custom_args={"model": get_initialized_model()},
234
+ ),
235
+ ),
236
+ "list_custom_component_installations": (
237
+ list_custom_component_installations_tool,
238
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
239
+ ),
240
+ "get_latest_custom_component_installation_logs": (
241
+ get_latest_custom_component_installation_logs_tool,
242
+ ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
243
+ ),
244
+ # Non-workspace tools
245
+ "list_component_families": (
246
+ list_component_families_tool,
247
+ ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
248
+ ),
249
+ "get_component_definition": (
250
+ get_component_definition_tool,
251
+ ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
252
+ ),
253
+ "search_component_definitions": (
254
+ search_component_definition_tool,
255
+ ToolConfig(
256
+ needs_client=True, memory_type=MemoryType.EXPLORABLE, custom_args={"model": get_initialized_model()}
257
+ ),
258
+ ),
259
+ "get_custom_components": (
260
+ get_custom_components_tool,
261
+ ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
262
+ ),
263
+ "list_secrets": (list_secrets_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
264
+ "get_secret": (get_secret_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
265
+ "list_workspaces": (list_workspaces_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
266
+ "get_workspace": (get_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
267
+ "create_workspace": (create_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
268
+ "get_from_object_store": (get_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
269
+ "get_slice_from_object_store": (get_slice_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
270
+ "search_docs": (search_docs, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
271
+ }
272
+
273
+
274
+ def create_enhanced_tool(
275
+ base_func: Callable[..., Any], config: ToolConfig, workspace_mode: WorkspaceMode, workspace: str | None = None
276
+ ) -> Callable[..., Awaitable[Any]]:
277
+ """Universal tool creator that handles client injection, workspace, and decorators.
278
+
279
+ This function takes a base tool function and enhances it based on a configuration.
280
+ It can inject a `client`, manage a `workspace` parameter (either explicitly required
281
+ or implicitly provided from the environment), and apply memory-related decorators.
282
+
283
+ It also supports partial application of custom arguments specified in the ToolConfig.
284
+ These arguments are bound to the function, and both the function signature and the
285
+ docstring are updated to hide these implementation details from the end user of the tool.
286
+
287
+ All parameters in the final tool signature are converted to be keyword-only to enforce
288
+ explicit naming of arguments in tool calls.
289
+
290
+ Args:
291
+ base_func: The base tool function.
292
+ config: Tool configuration specifying dependencies and custom arguments.
293
+ workspace_mode: How the workspace should be handled (implicit or explicit).
294
+ workspace: The workspace to use for implicit mode.
295
+
296
+ Returns:
297
+ An enhanced, awaitable tool function with an updated signature and docstring.
298
+ """
299
+ original_func = base_func
300
+
301
+ # If custom arguments are provided, create a wrapper that applies them.
302
+ # This wrapper preserves the original function's metadata so that decorators work correctly.
303
+ func_to_decorate: Any
304
+ if config.custom_args:
305
+
306
+ @functools.wraps(original_func)
307
+ async def func_with_custom_args(*args: Any, **kwargs: Any) -> Any:
308
+ # Create a partial function with the custom arguments bound.
309
+ partial_func = functools.partial(original_func, **(config.custom_args or {}))
310
+ # Await the result of the partial function call.
311
+ return await partial_func(**kwargs)
312
+
313
+ func_to_decorate = func_with_custom_args
314
+ else:
315
+ func_to_decorate = original_func
316
+
317
+ # Apply memory-related decorators to the (potentially wrapped) function
318
+ decorated_func = func_to_decorate
319
+ if config.memory_type != MemoryType.NO_MEMORY:
320
+ store = STORE
321
+ explorer = RichExplorer(store)
322
+
323
+ if config.memory_type == MemoryType.EXPLORABLE:
324
+ decorated_func = explorable(object_store=store, explorer=explorer)(decorated_func)
325
+ elif config.memory_type == MemoryType.REFERENCEABLE:
326
+ decorated_func = referenceable(object_store=store, explorer=explorer)(decorated_func)
327
+ elif config.memory_type == MemoryType.BOTH:
328
+ decorated_func = explorable_and_referenceable(object_store=store, explorer=explorer)(decorated_func)
329
+
330
+ # Determine the parameters to remove from the original function's signature
331
+ params_to_remove: set[str] = set()
332
+ if config.custom_args:
333
+ params_to_remove.update(config.custom_args.keys())
334
+ if config.needs_client:
335
+ params_to_remove.add("client")
336
+ if config.needs_workspace and workspace_mode == WorkspaceMode.IMPLICIT:
337
+ params_to_remove.add("workspace")
338
+
339
+ # Create the new signature from the original function
340
+ original_sig = inspect.signature(original_func)
341
+ final_params = [p for name, p in original_sig.parameters.items() if name not in params_to_remove]
342
+
343
+ # Convert all positional-or-keyword parameters to be keyword-only
344
+ keyword_only_params = [
345
+ p.replace(kind=inspect.Parameter.KEYWORD_ONLY) if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD else p
346
+ for p in final_params
347
+ ]
348
+ new_sig = original_sig.replace(parameters=keyword_only_params)
349
+
350
+ # Create the final wrapper function that handles client/workspace injection
351
+ if config.needs_client:
352
+ if config.needs_workspace:
353
+ if workspace_mode == WorkspaceMode.IMPLICIT:
354
+
355
+ async def workspace_implicit_wrapper(**kwargs: Any) -> Any:
356
+ ws = workspace or get_workspace_from_env()
357
+ async with AsyncDeepsetClient() as client:
358
+ return await decorated_func(client=client, workspace=ws, **kwargs)
359
+
360
+ wrapper = workspace_implicit_wrapper
361
+ else: # EXPLICIT mode
362
+
363
+ async def workspace_explicit_wrapper(**kwargs: Any) -> Any:
364
+ async with AsyncDeepsetClient() as client:
365
+ # The first argument is the workspace, which must be passed by keyword.
366
+ return await decorated_func(client=client, **kwargs)
367
+
368
+ wrapper = workspace_explicit_wrapper
369
+ else: # Client-only tools
370
+
371
+ async def client_only_wrapper(**kwargs: Any) -> Any:
372
+ async with AsyncDeepsetClient() as client:
373
+ return await decorated_func(client=client, **kwargs)
374
+
375
+ wrapper = client_only_wrapper
376
+ else: # No injection needed
377
+ if inspect.iscoroutinefunction(decorated_func):
378
+
379
+ async def no_injection_wrapper(**kwargs: Any) -> Any:
380
+ return await decorated_func(**kwargs)
381
+
382
+ wrapper = no_injection_wrapper
383
+ else:
384
+
385
+ @functools.wraps(decorated_func)
386
+ async def async_wrapper(**kwargs: Any) -> Any:
387
+ return decorated_func(**kwargs)
388
+
389
+ wrapper = async_wrapper
390
+
391
+ # Set metadata on the final wrapper
392
+ wrapper.__signature__ = new_sig # type: ignore
393
+ wrapper.__name__ = original_func.__name__
394
+
395
+ # Process the docstring to remove injected and partially applied parameters
396
+ if original_func.__doc__:
397
+ import re
398
+
399
+ doc = original_func.__doc__
400
+ params_to_remove_from_doc = set()
401
+ if config.needs_client:
402
+ params_to_remove_from_doc.add("client")
403
+ if config.needs_workspace and workspace_mode == WorkspaceMode.IMPLICIT:
404
+ params_to_remove_from_doc.add("workspace")
405
+ if config.custom_args:
406
+ params_to_remove_from_doc.update(config.custom_args.keys())
407
+
408
+ for param_name in params_to_remove_from_doc:
409
+ doc = re.sub(
410
+ rf"^\s*:param\s+{re.escape(param_name)}.*?(?=^\s*:|^\s*$|\Z)",
411
+ "",
412
+ doc,
413
+ flags=re.MULTILINE | re.DOTALL,
414
+ )
415
+
416
+ wrapper.__doc__ = "\n".join([line.rstrip() for line in doc.strip().split("\n")])
417
+ else:
418
+ wrapper.__doc__ = original_func.__doc__
419
+
420
+ return wrapper
421
+
422
+
423
+ def register_tools(
424
+ mcp: FastMCP, workspace_mode: WorkspaceMode, workspace: str | None = None, tool_names: set[str] | None = None
425
+ ) -> None:
426
+ """Register tools with unified configuration.
427
+
428
+ Args:
429
+ mcp: FastMCP server instance
430
+ workspace_mode: How workspace should be handled
431
+ workspace: Workspace to use for implicit mode (if None, reads from env)
432
+ tool_names: Set of tool names to register (if None, registers all tools)
433
+ """
434
+ # Check if docs search is available
435
+ docs_available = get_docs_config() is not None
436
+
437
+ # Validate tool names if provided
438
+ if tool_names is not None:
439
+ all_tools = set(TOOL_REGISTRY.keys())
440
+ invalid_tools = tool_names - all_tools
441
+ if invalid_tools:
442
+ sorted_invalid = sorted(invalid_tools)
443
+ sorted_all = sorted(all_tools)
444
+ raise ValueError(f"Unknown tools: {', '.join(sorted_invalid)}\nAvailable tools: {', '.join(sorted_all)}")
445
+
446
+ # Warn if search_docs was requested but config is missing
447
+ if "search_docs" in tool_names and not docs_available:
448
+ logging.warning(
449
+ "Documentation search tool requested but not available. To enable, set the following environment "
450
+ "variables: DEEPSET_DOCS_WORKSPACE, DEEPSET_DOCS_PIPELINE_NAME, DEEPSET_DOCS_API_KEY"
451
+ )
452
+
453
+ tools_to_register = tool_names.copy()
454
+ else:
455
+ tools_to_register = set(TOOL_REGISTRY.keys())
456
+
457
+ # Warn if search_docs would be skipped in "all tools" mode
458
+ if not docs_available:
459
+ logging.warning(
460
+ "Documentation search tool not enabled. To enable, set the following environment "
461
+ "variables: DEEPSET_DOCS_WORKSPACE, DEEPSET_DOCS_PIPELINE_NAME, DEEPSET_DOCS_API_KEY"
462
+ )
463
+
464
+ # Remove search_docs if config is not available
465
+ if not docs_available:
466
+ tools_to_register.discard("search_docs")
467
+
468
+ for tool_name in tools_to_register:
469
+ base_func, config = TOOL_REGISTRY[tool_name]
470
+ # Create enhanced tool
471
+ enhanced_tool = create_enhanced_tool(base_func, config, workspace_mode, workspace)
472
+
473
+ mcp.add_tool(enhanced_tool, name=tool_name)
File without changes
@@ -0,0 +1,52 @@
1
+ from deepset_mcp.api.custom_components.models import CustomComponentInstallationList
2
+ from deepset_mcp.api.protocols import AsyncClientProtocol
3
+
4
+
5
+ async def list_custom_component_installations(
6
+ *, client: AsyncClientProtocol, workspace: str
7
+ ) -> CustomComponentInstallationList | str:
8
+ """List custom component installations.
9
+
10
+ :param client: The API client to use.
11
+ :param workspace: The workspace to operate in.
12
+
13
+ :returns: Custom component installations or error message.
14
+ """
15
+ custom_components = client.custom_components(workspace)
16
+ users = client.users()
17
+
18
+ try:
19
+ installations = await custom_components.list_installations()
20
+ except Exception as e:
21
+ return f"Failed to retrieve custom component installations: {e}"
22
+
23
+ # Enrich installations with user information
24
+ for installation in installations.data:
25
+ if installation.created_by_user_id:
26
+ try:
27
+ user = await users.get(installation.created_by_user_id)
28
+ installation.user_info = user
29
+ except Exception:
30
+ # If user fetch fails, user_info remains None
31
+ pass
32
+
33
+ return installations
34
+
35
+
36
+ async def get_latest_custom_component_installation_logs(*, client: AsyncClientProtocol, workspace: str) -> str:
37
+ """Get the logs from the latest custom component installation.
38
+
39
+ :param client: The API client to use.
40
+ :param workspace: The workspace to operate in.
41
+
42
+ :returns: The latest installation logs or error message.
43
+ """
44
+ custom_components = client.custom_components(workspace)
45
+
46
+ try:
47
+ logs = await custom_components.get_latest_installation_logs()
48
+ if not logs:
49
+ return "No installation logs found."
50
+ return logs
51
+ except Exception as e:
52
+ return f"Failed to retrieve latest installation logs: {e}"
@@ -0,0 +1,83 @@
1
+ import os
2
+
3
+ from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
4
+ from deepset_mcp.api.pipeline.models import DeepsetSearchResponse
5
+ from deepset_mcp.api.protocols import AsyncClientProtocol
6
+
7
+
8
+ def doc_search_results_to_llm_readable_string(*, results: DeepsetSearchResponse) -> str:
9
+ """Formats results of the doc search pipeline so that they can be read by an LLM.
10
+
11
+ :param results: DeepsetSearchResponse object
12
+ :return: Formatted results.
13
+ """
14
+ file_segmented_docs = []
15
+
16
+ previous_source_id = None
17
+ for doc in results.documents:
18
+ if previous_source_id != doc.meta["source_id"]:
19
+ file_segmented_docs.append([{"content": doc.content, "file_path": doc.meta.get("original_file_path", "")}])
20
+ previous_source_id = doc.meta.get("source_id")
21
+ else:
22
+ file_segmented_docs[-1].append(
23
+ {"content": doc.content, "file_path": doc.meta.get("original_file_path", "")}
24
+ )
25
+
26
+ files = []
27
+ for file_docs in file_segmented_docs:
28
+ start = file_docs[0]["file_path"]
29
+ full_doc = " ".join([doc["content"] for doc in file_docs])
30
+ files.append(start + "\n" + full_doc)
31
+
32
+ return "\n----\n".join(files)
33
+
34
+
35
+ async def search_docs(*, client: AsyncClientProtocol, workspace: str, pipeline_name: str, query: str) -> str:
36
+ """Search deepset documentation using a dedicated docs pipeline.
37
+
38
+ Uses the specified pipeline to perform a search with the given query against the deepset
39
+ documentation. Before executing the search, checks if the pipeline is deployed (status = DEPLOYED).
40
+ Returns search results in a human-readable format.
41
+
42
+ :param client: The async client for API communication.
43
+ :param workspace: The workspace name for the docs pipeline.
44
+ :param pipeline_name: Name of the pipeline to use for doc search.
45
+ :param query: The search query to execute.
46
+ :returns: A string containing the formatted search results or error message.
47
+ """
48
+ try:
49
+ # First, check if the pipeline exists and get its status
50
+ pipeline = await client.pipelines(workspace=workspace).get(pipeline_name=pipeline_name)
51
+
52
+ # Check if pipeline is deployed
53
+ if pipeline.status != "DEPLOYED":
54
+ return f"Documentation pipeline '{pipeline_name}' is not deployed (current status: {pipeline.status})."
55
+
56
+ # Execute the search
57
+ search_response = await client.pipelines(workspace=workspace).search(pipeline_name=pipeline_name, query=query)
58
+
59
+ return doc_search_results_to_llm_readable_string(results=search_response)
60
+
61
+ except ResourceNotFoundError:
62
+ return f"There is no documentation pipeline named '{pipeline_name}' in workspace '{workspace}'."
63
+ except BadRequestError as e:
64
+ return f"Failed to search documentation using pipeline '{pipeline_name}': {e}"
65
+ except UnexpectedAPIError as e:
66
+ return f"Failed to search documentation using pipeline '{pipeline_name}': {e}"
67
+ except Exception as e:
68
+ return f"An unexpected error occurred while searching documentation with pipeline '{pipeline_name}': {str(e)}"
69
+
70
+
71
+ def get_docs_config() -> tuple[str, str, str] | None:
72
+ """Get docs search configuration from environment variables.
73
+
74
+ :returns: Tuple of (workspace, pipeline_name, api_key) if all are available, None otherwise.
75
+ """
76
+ workspace = os.environ.get("DEEPSET_DOCS_WORKSPACE")
77
+ pipeline_name = os.environ.get("DEEPSET_DOCS_PIPELINE_NAME")
78
+ api_key = os.environ.get("DEEPSET_DOCS_API_KEY")
79
+
80
+ if workspace and pipeline_name and api_key:
81
+ return workspace, pipeline_name, api_key
82
+
83
+ return None