deepset-mcp 0.0.4rc1__py3-none-any.whl → 0.0.5rc1__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.
@@ -6,459 +6,327 @@
6
6
 
7
7
  import functools
8
8
  import inspect
9
- import logging
10
- import os
9
+ import re
11
10
  from collections.abc import Awaitable, Callable
12
- from dataclasses import dataclass
13
- from enum import StrEnum
14
11
  from typing import Any
15
12
 
16
- from mcp.server.fastmcp import FastMCP
13
+ from mcp.server.fastmcp import Context, FastMCP
17
14
 
18
15
  from deepset_mcp.api.client import AsyncDeepsetClient
19
- from deepset_mcp.config import DEFAULT_CLIENT_HEADER
20
- from deepset_mcp.initialize_embedding_model import get_initialized_model
21
- from deepset_mcp.store import STORE
22
- from deepset_mcp.tools.custom_components import (
23
- get_latest_custom_component_installation_logs as get_latest_custom_component_installation_logs_tool,
24
- list_custom_component_installations as list_custom_component_installations_tool,
25
- )
26
- from deepset_mcp.tools.doc_search import (
27
- search_docs as search_docs_tool,
28
- )
29
- from deepset_mcp.tools.haystack_service import (
30
- get_component_definition as get_component_definition_tool,
31
- get_custom_components as get_custom_components_tool,
32
- list_component_families as list_component_families_tool,
33
- search_component_definition as search_component_definition_tool,
34
- )
35
- from deepset_mcp.tools.indexes import (
36
- create_index as create_index_tool,
37
- deploy_index as deploy_index_tool,
38
- get_index as get_index_tool,
39
- list_indexes as list_indexes_tool,
40
- update_index as update_index_tool,
16
+ from deepset_mcp.config import DEFAULT_CLIENT_HEADER, DOCS_SEARCH_TOOL_NAME
17
+ from deepset_mcp.tool_models import DeepsetDocsConfig, MemoryType, ToolConfig, WorkspaceMode
18
+ from deepset_mcp.tool_registry import TOOL_REGISTRY
19
+ from deepset_mcp.tools.tokonomics import (
20
+ ObjectStore,
21
+ RichExplorer,
22
+ explorable,
23
+ explorable_and_referenceable,
24
+ referenceable,
41
25
  )
42
26
 
43
- # Import all tool functions
44
- from deepset_mcp.tools.pipeline import (
45
- create_pipeline as create_pipeline_tool,
46
- deploy_pipeline as deploy_pipeline_tool,
47
- get_pipeline as get_pipeline_tool,
48
- get_pipeline_logs as get_pipeline_logs_tool,
49
- list_pipelines as list_pipelines_tool,
50
- search_pipeline as search_pipeline_tool,
51
- update_pipeline as update_pipeline_tool,
52
- validate_pipeline as validate_pipeline_tool,
53
- )
54
- from deepset_mcp.tools.pipeline_template import (
55
- get_template as get_pipeline_template_tool,
56
- list_templates as list_pipeline_templates_tool,
57
- search_templates as search_pipeline_templates_tool,
58
- )
59
- from deepset_mcp.tools.secrets import (
60
- get_secret as get_secret_tool,
61
- list_secrets as list_secrets_tool,
62
- )
63
- from deepset_mcp.tools.tokonomics import RichExplorer, explorable, explorable_and_referenceable, referenceable
64
- from deepset_mcp.tools.workspace import (
65
- create_workspace as create_workspace_tool,
66
- get_workspace as get_workspace_tool,
67
- list_workspaces as list_workspaces_tool,
68
- )
69
27
 
28
+ def apply_custom_args(base_func: Callable[..., Any], config: ToolConfig) -> Callable[..., Any]:
29
+ """
30
+ Applies custom keyword arguments defined in the ToolConfig to a function.
70
31
 
71
- def are_docs_available() -> bool:
72
- """Checks if documentation search is available."""
73
- return bool(
74
- os.environ.get("DEEPSET_DOCS_WORKSPACE", False)
75
- and os.environ.get("DEEPSET_DOCS_PIPELINE_NAME", False)
76
- and os.environ.get("DEEPSET_DOCS_API_KEY", False)
77
- )
32
+ Removes the partially applied keyword arguments from the function's signature and docstring.
78
33
 
34
+ :param base_func: The function to apply custom keyword arguments to.
35
+ :param config: The ToolConfig for the function.
36
+ :returns: Function with custom arguments applied and updated signature/docstring.
37
+ """
38
+ if not config.custom_args:
39
+ return base_func
79
40
 
80
- EXPLORER = RichExplorer(store=STORE)
41
+ @functools.wraps(base_func)
42
+ async def func_with_custom_args(*args: Any, **kwargs: Any) -> Any:
43
+ # Create a partial function with the custom arguments bound.
44
+ partial_func = functools.partial(base_func, **(config.custom_args or {}))
45
+ # Await the result of the partial function call.
46
+ return await partial_func(**kwargs)
81
47
 
48
+ # Remove custom args from signature
49
+ original_sig = inspect.signature(base_func)
50
+ new_params = [p for name, p in original_sig.parameters.items() if name not in config.custom_args]
51
+ func_with_custom_args.__signature__ = original_sig.replace(parameters=new_params) # type: ignore
82
52
 
83
- def get_from_object_store(object_id: str, path: str = "") -> str:
84
- """Use this tool to fetch an object from the object store.
53
+ # Remove custom args from docstring.
54
+ func_with_custom_args.__doc__ = remove_params_from_docstring(base_func.__doc__, set(config.custom_args.keys()))
85
55
 
86
- You can fetch a specific object by using the object's id (e.g. `@obj_001`).
87
- You can also fetch any nested path by using the path-parameter
88
- (e.g. `{"object_id": "@obj_001", "path": "user_info.given_name"}`
89
- -> returns the content at obj.user_info.given_name).
56
+ return func_with_custom_args
90
57
 
91
- :param object_id: The id of the object to fetch in the format `@obj_001`.
92
- :param path: The path of the object to fetch in the format of `access.to.attr` or `["access"]["to"]["attr"]`.
93
- """
94
- return EXPLORER.explore(obj_id=object_id, path=path)
95
-
96
-
97
- def get_slice_from_object_store(
98
- object_id: str,
99
- start: int = 0,
100
- end: int | None = None,
101
- path: str = "",
102
- ) -> str:
103
- """Extract a slice from a string or list object that is stored in the object store.
104
-
105
- :param object_id: Identifier of the object.
106
- :param start: Start index for slicing.
107
- :param end: End index for slicing (optional - leave empty to get slice from start to end of sequence).
108
- :param path: Navigation path to object to slice (optional).
109
- :return: String representation of the slice.
58
+
59
+ def remove_params_from_docstring(docstring: str | None, params_to_remove: set[str]) -> str:
60
+ """Removes specified parameters from a function's docstring.
61
+
62
+ :param docstring: The docstring to remove the parameters from.
63
+ :param params_to_remove: The set of parameters to remove.
64
+ :returns: The changed docstring.
110
65
  """
111
- return EXPLORER.slice(obj_id=object_id, start=start, end=end, path=path)
66
+ if docstring is None:
67
+ return ""
68
+
69
+ for param_name in params_to_remove:
70
+ docstring = re.sub(
71
+ rf"^\s*:param\s+{re.escape(param_name)}.*?(?=^\s*:|^\s*$|\Z)",
72
+ "",
73
+ docstring,
74
+ flags=re.MULTILINE | re.DOTALL,
75
+ )
76
+
77
+ return "\n".join([line.rstrip() for line in docstring.strip().split("\n")])
112
78
 
113
79
 
114
- async def search_docs(query: str) -> str:
115
- """Search the deepset platform documentation.
80
+ def apply_workspace(
81
+ base_func: Callable[..., Any], config: ToolConfig, workspace_mode: WorkspaceMode, workspace: str | None = None
82
+ ) -> Callable[..., Any]:
83
+ """
84
+ Applies a deepset workspace to the function depending on the workspace mode and the ToolConfig.
116
85
 
117
- This tool allows you to search through deepset's official documentation to find
118
- information about features, API usage, best practices, and troubleshooting guides.
119
- Use this when you need to look up specific deepset functionality or help users
120
- understand how to use deepset features.
86
+ Removes the workspace argument from the function's signature and docstring if applied.
121
87
 
122
- :param query: The search query to execute against the documentation.
123
- :returns: The formatted search results from the documentation.
88
+ :param base_func: The function to apply workspace to.
89
+ :param config: The ToolConfig for the function.
90
+ :param workspace_mode: The WorkspaceMode for the function.
91
+ :param workspace: The workspace to use for static mode.
92
+ :returns: Function with workspace handling applied and updated signature/docstring.
93
+ :raises ValueError: If workspace is required but not available.
124
94
  """
125
- async with AsyncDeepsetClient(
126
- api_key=os.environ["DEEPSET_DOCS_API_KEY"], transport_config=DEFAULT_CLIENT_HEADER
127
- ) as client:
128
- response = await search_docs_tool(
129
- client=client,
130
- workspace=os.environ["DEEPSET_DOCS_WORKSPACE"],
131
- pipeline_name=os.environ["DEEPSET_DOCS_PIPELINE_NAME"],
132
- query=query,
133
- )
134
- return response
135
-
136
-
137
- class WorkspaceMode(StrEnum):
138
- """Configuration for how workspace is provided to tools."""
139
-
140
- STATIC = "static" # workspace from env, no parameter in tool signature
141
- DYNAMIC = "dynamic" # workspace as required parameter in tool signature
142
-
143
-
144
- class MemoryType(StrEnum):
145
- """Configuration for how memory is provided to tools."""
146
-
147
- EXPLORABLE = "explorable"
148
- REFERENCEABLE = "referenceable"
149
- BOTH = "both"
150
- NO_MEMORY = "no_memory"
151
-
152
-
153
- @dataclass
154
- class ToolConfig:
155
- """Configuration for tool registration."""
156
-
157
- needs_client: bool = False
158
- needs_workspace: bool = False
159
- memory_type: MemoryType = MemoryType.NO_MEMORY
160
- custom_args: dict[str, Any] | None = None # For special cases like search_component_definition
161
-
162
-
163
- def get_workspace_from_env() -> str:
164
- """Gets the workspace configured from environment variable."""
165
- workspace = os.environ.get("DEEPSET_WORKSPACE")
166
- if not workspace:
167
- raise ValueError("DEEPSET_WORKSPACE environment variable not set")
168
- return workspace
169
-
170
-
171
- TOOL_REGISTRY: dict[str, tuple[Callable[..., Any], ToolConfig]] = {
172
- # Workspace tools
173
- "list_pipelines": (
174
- list_pipelines_tool,
175
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
176
- ),
177
- "create_pipeline": (
178
- create_pipeline_tool,
179
- ToolConfig(
180
- needs_client=True,
181
- needs_workspace=True,
182
- memory_type=MemoryType.BOTH,
183
- custom_args={"skip_validation_errors": True},
184
- ),
185
- ),
186
- "update_pipeline": (
187
- update_pipeline_tool,
188
- ToolConfig(
189
- needs_client=True,
190
- needs_workspace=True,
191
- memory_type=MemoryType.BOTH,
192
- custom_args={"skip_validation_errors": True},
193
- ),
194
- ),
195
- "get_pipeline": (
196
- get_pipeline_tool,
197
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
198
- ),
199
- "deploy_pipeline": (
200
- deploy_pipeline_tool,
201
- ToolConfig(
202
- needs_client=True,
203
- needs_workspace=True,
204
- memory_type=MemoryType.EXPLORABLE,
205
- custom_args={"wait_for_deployment": True, "timeout_seconds": 600, "poll_interval": 5},
206
- ),
207
- ),
208
- "validate_pipeline": (
209
- validate_pipeline_tool,
210
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
211
- ),
212
- "get_pipeline_logs": (
213
- get_pipeline_logs_tool,
214
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
215
- ),
216
- "search_pipeline": (
217
- search_pipeline_tool,
218
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
219
- ),
220
- "list_indexes": (
221
- list_indexes_tool,
222
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
223
- ),
224
- "get_index": (
225
- get_index_tool,
226
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
227
- ),
228
- "create_index": (
229
- create_index_tool,
230
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
231
- ),
232
- "update_index": (
233
- update_index_tool,
234
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.BOTH),
235
- ),
236
- "deploy_index": (
237
- deploy_index_tool,
238
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
239
- ),
240
- "list_templates": (
241
- list_pipeline_templates_tool,
242
- ToolConfig(
243
- needs_client=True,
244
- needs_workspace=True,
245
- memory_type=MemoryType.EXPLORABLE,
246
- custom_args={"field": "created_at", "order": "DESC", "limit": 100},
247
- ),
248
- ),
249
- "get_template": (
250
- get_pipeline_template_tool,
251
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
252
- ),
253
- "search_templates": (
254
- search_pipeline_templates_tool,
255
- ToolConfig(
256
- needs_client=True,
257
- needs_workspace=True,
258
- memory_type=MemoryType.EXPLORABLE,
259
- custom_args={"model": get_initialized_model()},
260
- ),
261
- ),
262
- "list_custom_component_installations": (
263
- list_custom_component_installations_tool,
264
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
265
- ),
266
- "get_latest_custom_component_installation_logs": (
267
- get_latest_custom_component_installation_logs_tool,
268
- ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
269
- ),
270
- # Non-workspace tools
271
- "list_component_families": (
272
- list_component_families_tool,
273
- ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
274
- ),
275
- "get_component_definition": (
276
- get_component_definition_tool,
277
- ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
278
- ),
279
- "search_component_definitions": (
280
- search_component_definition_tool,
281
- ToolConfig(
282
- needs_client=True, memory_type=MemoryType.EXPLORABLE, custom_args={"model": get_initialized_model()}
283
- ),
284
- ),
285
- "get_custom_components": (
286
- get_custom_components_tool,
287
- ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE),
288
- ),
289
- "list_secrets": (list_secrets_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
290
- "get_secret": (get_secret_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
291
- "list_workspaces": (list_workspaces_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
292
- "get_workspace": (get_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
293
- "create_workspace": (create_workspace_tool, ToolConfig(needs_client=True, memory_type=MemoryType.EXPLORABLE)),
294
- "get_from_object_store": (get_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
295
- "get_slice_from_object_store": (get_slice_from_object_store, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
296
- "search_docs": (search_docs, ToolConfig(memory_type=MemoryType.NO_MEMORY)),
297
- }
298
-
299
-
300
- def create_enhanced_tool(
301
- base_func: Callable[..., Any], config: ToolConfig, workspace_mode: WorkspaceMode, workspace: str | None = None
302
- ) -> Callable[..., Awaitable[Any]]:
303
- """Universal tool creator that handles client injection, workspace, and decorators.
95
+ if not config.needs_workspace:
96
+ return base_func
304
97
 
305
- This function takes a base tool function and enhances it based on a configuration.
306
- It can inject a `client`, manage a `workspace` parameter (either explicitly required
307
- or implicitly provided from the environment), and apply memory-related decorators.
98
+ if workspace_mode == WorkspaceMode.STATIC:
308
99
 
309
- It also supports partial application of custom arguments specified in the ToolConfig.
310
- These arguments are bound to the function, and both the function signature and the
311
- docstring are updated to hide these implementation details from the end user of the tool.
100
+ @functools.wraps(base_func)
101
+ async def workspace_wrapper(*args: Any, **kwargs: Any) -> Any:
102
+ return await base_func(*args, workspace=workspace, **kwargs)
312
103
 
313
- All parameters in the final tool signature are converted to be keyword-only to enforce
314
- explicit naming of arguments in tool calls.
104
+ # Remove workspace from signature
105
+ original_sig = inspect.signature(base_func)
106
+ new_params = [p for name, p in original_sig.parameters.items() if name != "workspace"]
107
+ workspace_wrapper.__signature__ = original_sig.replace(parameters=new_params) # type: ignore
315
108
 
316
- Args:
317
- base_func: The base tool function.
318
- config: Tool configuration specifying dependencies and custom arguments.
319
- workspace_mode: How the workspace should be handled.
320
- workspace: The workspace to use when using a static workspace.
109
+ # Remove workspace from docstring
110
+ workspace_wrapper.__doc__ = remove_params_from_docstring(base_func.__doc__, {"workspace"})
111
+
112
+ return workspace_wrapper
113
+ else:
114
+ # For dynamic mode, workspace is passed as parameter
115
+ return base_func
116
+
117
+
118
+ def apply_memory(
119
+ base_func: Callable[..., Any], config: ToolConfig, store: ObjectStore | None = None
120
+ ) -> Callable[..., Any]:
121
+ """
122
+ Applies memory decorators to a function if requested in the ToolConfig.
321
123
 
322
- Returns:
323
- An enhanced, awaitable tool function with an updated signature and docstring.
124
+ :param base_func: The function to apply memory decorator to.
125
+ :param config: The ToolConfig for the function.
126
+ :param store: The ObjectStore instance to use
127
+ :returns: Function with memory decorators applied.
128
+ :raises ValueError: If an invalid memory type is specified.
324
129
  """
325
- original_func = base_func
130
+ if config.memory_type == MemoryType.NO_MEMORY:
131
+ return base_func
326
132
 
327
- # If custom arguments are provided, create a wrapper that applies them.
328
- # This wrapper preserves the original function's metadata so that decorators work correctly.
329
- func_to_decorate: Any
330
- if config.custom_args:
133
+ if store is None:
134
+ raise ValueError("ObjectStore instance is required for memory decorators")
331
135
 
332
- @functools.wraps(original_func)
333
- async def func_with_custom_args(*args: Any, **kwargs: Any) -> Any:
334
- # Create a partial function with the custom arguments bound.
335
- partial_func = functools.partial(original_func, **(config.custom_args or {}))
336
- # Await the result of the partial function call.
337
- return await partial_func(**kwargs)
136
+ explorer = RichExplorer(store)
338
137
 
339
- func_to_decorate = func_with_custom_args
138
+ if config.memory_type == MemoryType.EXPLORABLE:
139
+ return explorable(object_store=store, explorer=explorer)(base_func)
140
+ elif config.memory_type == MemoryType.REFERENCEABLE:
141
+ return referenceable(object_store=store, explorer=explorer)(base_func)
142
+ elif config.memory_type == MemoryType.BOTH:
143
+ return explorable_and_referenceable(object_store=store, explorer=explorer)(base_func)
340
144
  else:
341
- func_to_decorate = original_func
342
-
343
- # Apply memory-related decorators to the (potentially wrapped) function
344
- decorated_func = func_to_decorate
345
- if config.memory_type != MemoryType.NO_MEMORY:
346
- store = STORE
347
- explorer = RichExplorer(store)
348
-
349
- if config.memory_type == MemoryType.EXPLORABLE:
350
- decorated_func = explorable(object_store=store, explorer=explorer)(decorated_func)
351
- elif config.memory_type == MemoryType.REFERENCEABLE:
352
- decorated_func = referenceable(object_store=store, explorer=explorer)(decorated_func)
353
- elif config.memory_type == MemoryType.BOTH:
354
- decorated_func = explorable_and_referenceable(object_store=store, explorer=explorer)(decorated_func)
355
-
356
- # Determine the parameters to remove from the original function's signature
357
- params_to_remove: set[str] = set()
358
- if config.custom_args:
359
- params_to_remove.update(config.custom_args.keys())
360
- if config.needs_client:
361
- params_to_remove.add("client")
362
- if config.needs_workspace and workspace_mode == WorkspaceMode.STATIC:
363
- params_to_remove.add("workspace")
364
-
365
- # Create the new signature from the original function
366
- original_sig = inspect.signature(original_func)
367
- final_params = [p for name, p in original_sig.parameters.items() if name not in params_to_remove]
368
-
369
- # Convert all positional-or-keyword parameters to be keyword-only
370
- keyword_only_params = [
371
- p.replace(kind=inspect.Parameter.KEYWORD_ONLY) if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD else p
372
- for p in final_params
373
- ]
374
- new_sig = original_sig.replace(parameters=keyword_only_params)
375
-
376
- # Create the final wrapper function that handles client/workspace injection
377
- if config.needs_client:
378
- if config.needs_workspace:
379
- if workspace_mode == WorkspaceMode.STATIC:
380
-
381
- async def workspace_environment_wrapper(**kwargs: Any) -> Any:
382
- ws = workspace or get_workspace_from_env()
383
- async with AsyncDeepsetClient(transport_config=DEFAULT_CLIENT_HEADER) as client:
384
- return await decorated_func(client=client, workspace=ws, **kwargs)
385
-
386
- wrapper = workspace_environment_wrapper
387
- else: # DYNAMIC mode
388
-
389
- async def workspace_explicit_wrapper(**kwargs: Any) -> Any:
390
- async with AsyncDeepsetClient(transport_config=DEFAULT_CLIENT_HEADER) as client:
391
- # The first argument is the workspace, which must be passed by keyword.
392
- return await decorated_func(client=client, **kwargs)
393
-
394
- wrapper = workspace_explicit_wrapper
395
- else: # Client-only tools
396
-
397
- async def client_only_wrapper(**kwargs: Any) -> Any:
398
- async with AsyncDeepsetClient(transport_config=DEFAULT_CLIENT_HEADER) as client:
399
- return await decorated_func(client=client, **kwargs)
400
-
401
- wrapper = client_only_wrapper
402
- else: # No injection needed
403
- if inspect.iscoroutinefunction(decorated_func):
404
-
405
- async def no_injection_wrapper(**kwargs: Any) -> Any:
406
- return await decorated_func(**kwargs)
407
-
408
- wrapper = no_injection_wrapper
409
- else:
145
+ raise ValueError(f"Invalid memory type: {config.memory_type}")
410
146
 
411
- @functools.wraps(decorated_func)
412
- async def async_wrapper(**kwargs: Any) -> Any:
413
- return decorated_func(**kwargs)
414
-
415
- wrapper = async_wrapper
416
-
417
- # Set metadata on the final wrapper
418
- wrapper.__signature__ = new_sig # type: ignore
419
- wrapper.__name__ = original_func.__name__
420
-
421
- # Process the docstring to remove injected and partially applied parameters
422
- if original_func.__doc__:
423
- import re
424
-
425
- doc = original_func.__doc__
426
- params_to_remove_from_doc = set()
427
- if config.needs_client:
428
- params_to_remove_from_doc.add("client")
429
- if config.needs_workspace and workspace_mode == WorkspaceMode.STATIC:
430
- params_to_remove_from_doc.add("workspace")
431
- if config.custom_args:
432
- params_to_remove_from_doc.update(config.custom_args.keys())
433
-
434
- for param_name in params_to_remove_from_doc:
435
- doc = re.sub(
436
- rf"^\s*:param\s+{re.escape(param_name)}.*?(?=^\s*:|^\s*$|\Z)",
437
- "",
438
- doc,
439
- flags=re.MULTILINE | re.DOTALL,
440
- )
441
147
 
442
- wrapper.__doc__ = "\n".join([line.rstrip() for line in doc.strip().split("\n")])
148
+ def apply_client(
149
+ base_func: Callable[..., Any],
150
+ config: ToolConfig,
151
+ use_request_context: bool = True,
152
+ base_url: str | None = None,
153
+ api_key: str | None = None,
154
+ ) -> Callable[..., Any]:
155
+ """
156
+ Applies the deepset API client to a function.
157
+
158
+ Optionally collects the API key from the request context, when use_request_context is True.
159
+ Modifies the function's signature and docstring to remove the client argument.
160
+ Adds a 'ctx' argument to the signature if the request context is used.
161
+
162
+ :param base_func: The function to apply the client to.
163
+ :param config: The ToolConfig for the function.
164
+ :param use_request_context: Whether to collect the API key from the request context.
165
+ :param base_url: Base URL for the deepset API.
166
+ :param api_key: The API key to use.
167
+ :returns: Function with client injection applied and updated signature/docstring.
168
+ :raises ValueError: If API key cannot be extracted from request context.
169
+ """
170
+ if not config.needs_client:
171
+ return base_func
172
+
173
+ if use_request_context:
174
+
175
+ @functools.wraps(base_func)
176
+ async def client_wrapper_with_context(*args: Any, **kwargs: Any) -> Any:
177
+ ctx = kwargs.pop("ctx", None)
178
+ if not ctx:
179
+ raise ValueError("Context is required for client authentication")
180
+
181
+ api_key = ctx.request_context.request.headers.get("Authorization")
182
+ if not api_key:
183
+ raise ValueError("No Authorization header found in request context")
184
+
185
+ api_key = api_key.replace("Bearer ", "")
186
+
187
+ if not api_key:
188
+ raise ValueError("API key cannot be empty")
189
+
190
+ client_kwargs: dict[str, Any] = {"transport_config": DEFAULT_CLIENT_HEADER, "api_key": api_key}
191
+ if base_url:
192
+ client_kwargs["base_url"] = base_url
193
+ async with AsyncDeepsetClient(**client_kwargs) as client:
194
+ return await base_func(*args, client=client, **kwargs)
195
+
196
+ # Remove client from signature and add ctx
197
+ original_sig = inspect.signature(base_func)
198
+ new_params = [p for name, p in original_sig.parameters.items() if name != "client"]
199
+ ctx_param = inspect.Parameter(name="ctx", kind=inspect.Parameter.KEYWORD_ONLY, annotation=Context)
200
+ new_params.append(ctx_param)
201
+ client_wrapper_with_context.__signature__ = original_sig.replace(parameters=new_params) # type: ignore
202
+ client_wrapper_with_context.__doc__ = remove_params_from_docstring(base_func.__doc__, {"client"})
203
+
204
+ return client_wrapper_with_context
443
205
  else:
444
- wrapper.__doc__ = original_func.__doc__
445
206
 
446
- return wrapper
207
+ @functools.wraps(base_func)
208
+ async def client_wrapper_without_context(*args: Any, **kwargs: Any) -> Any:
209
+ client_kwargs: dict[str, Any] = {"transport_config": DEFAULT_CLIENT_HEADER, "api_key": api_key}
210
+ if base_url:
211
+ client_kwargs["base_url"] = base_url
212
+ async with AsyncDeepsetClient(**client_kwargs) as client:
213
+ return await base_func(*args, client=client, **kwargs)
214
+
215
+ # Remove client from signature
216
+ original_sig = inspect.signature(base_func)
217
+ new_params = [p for name, p in original_sig.parameters.items() if name != "client"]
218
+ client_wrapper_without_context.__signature__ = original_sig.replace(parameters=new_params) # type: ignore
219
+
220
+ # Remove client from docstring
221
+ client_wrapper_without_context.__doc__ = remove_params_from_docstring(base_func.__doc__, {"client"})
222
+
223
+ return client_wrapper_without_context
224
+
225
+
226
+ def build_tool(
227
+ base_func: Callable[..., Any],
228
+ config: ToolConfig,
229
+ workspace_mode: WorkspaceMode,
230
+ api_key: str | None = None,
231
+ workspace: str | None = None,
232
+ use_request_context: bool = True,
233
+ base_url: str | None = None,
234
+ object_store: ObjectStore | None = None,
235
+ ) -> Callable[..., Awaitable[Any]]:
236
+ """
237
+ Universal tool creator that handles client injection, workspace, and decorators.
238
+
239
+ This function takes a base tool function and enhances it based on the tool's configuration.
240
+
241
+ :param base_func: The base tool function.
242
+ :param config: Tool configuration specifying dependencies and custom arguments.
243
+ :param workspace_mode: How the workspace should be handled.
244
+ :param api_key: The deepset API key to use.
245
+ :param workspace: The workspace to use when using a static workspace.
246
+ :param use_request_context: Whether to collect the API key from the request context.
247
+ :param base_url: Base URL for the deepset API.
248
+ :param object_store: The ObjectStore instance to use for memory decorators.
249
+ :returns: An enhanced, awaitable tool function with an updated signature and docstring.
250
+ """
251
+ enhanced_func = base_func
252
+
253
+ # Apply custom arguments first
254
+ enhanced_func = apply_custom_args(enhanced_func, config)
255
+
256
+ # Apply memory decorators with the provided store
257
+ enhanced_func = apply_memory(enhanced_func, config, object_store)
258
+
259
+ # Apply workspace handling
260
+ enhanced_func = apply_workspace(enhanced_func, config, workspace_mode, workspace)
261
+
262
+ # Apply client injection (adds ctx parameter if needed)
263
+ enhanced_func = apply_client(
264
+ enhanced_func, config, use_request_context=use_request_context, base_url=base_url, api_key=api_key
265
+ )
266
+
267
+ # Create final async wrapper if needed
268
+ if not inspect.iscoroutinefunction(enhanced_func):
269
+
270
+ @functools.wraps(enhanced_func)
271
+ async def async_wrapper(**kwargs: Any) -> Any:
272
+ return enhanced_func(**kwargs)
273
+
274
+ # Copy over the signature from the enhanced function
275
+ async_wrapper.__signature__ = inspect.signature(enhanced_func) # type: ignore
276
+ return async_wrapper
277
+
278
+ enhanced_func.__name__ = base_func.__name__
279
+
280
+ return enhanced_func
447
281
 
448
282
 
449
283
  def register_tools(
450
- mcp: FastMCP, workspace_mode: WorkspaceMode, workspace: str | None = None, tool_names: set[str] | None = None
284
+ mcp_server_instance: FastMCP,
285
+ workspace_mode: WorkspaceMode,
286
+ api_key: str | None = None,
287
+ workspace: str | None = None,
288
+ tool_names: set[str] | None = None,
289
+ get_api_key_from_authorization_header: bool = True,
290
+ docs_config: DeepsetDocsConfig | None = None,
291
+ base_url: str | None = None,
292
+ object_store: ObjectStore | None = None,
451
293
  ) -> None:
452
294
  """Register tools with unified configuration.
453
295
 
454
296
  Args:
455
- mcp: FastMCP server instance
297
+ mcp_server_instance: FastMCP server instance
456
298
  workspace_mode: How workspace should be handled
457
- workspace: Workspace to use for environment mode (if None, reads from env)
299
+ api_key: An api key for the deepset AI platform; only needs to be provided when not read from request context.
300
+ workspace: Workspace to use; only needs to be provided if using a static workspace.
458
301
  tool_names: Set of tool names to register (if None, registers all tools)
302
+ get_api_key_from_authorization_header: Whether to use request context to retrieve an API key for tool execution.
303
+ docs_config: Configuration for the deepset documentation search tool.
304
+ base_url: Base URL for the deepset API.
305
+ object_store: The ObjectStore instance to use for memory decorators.
459
306
  """
460
- # Check if docs search is available
461
- docs_available = are_docs_available()
307
+ if api_key is None and not get_api_key_from_authorization_header:
308
+ raise ValueError(
309
+ "'api_key' cannot be 'None' when 'use_request_context' is False. "
310
+ "Either pass 'api_key' or 'use_request_context'."
311
+ )
312
+
313
+ if workspace_mode == WorkspaceMode.STATIC and workspace is None:
314
+ raise ValueError(
315
+ "'workspace_mode' set to 'static' but no workspace provided. "
316
+ "You need to set a deepset workspace name as 'workspace'."
317
+ )
318
+
319
+ if docs_config is None and tool_names is None:
320
+ raise ValueError(
321
+ f"'docs_config' cannot be None when requesting to register all tools. "
322
+ f"Either pass 'docs_config' or disable the '{DOCS_SEARCH_TOOL_NAME}' tool."
323
+ )
324
+
325
+ if docs_config is None and tool_names is not None and DOCS_SEARCH_TOOL_NAME in tool_names:
326
+ raise ValueError(
327
+ f"Requested to register '{DOCS_SEARCH_TOOL_NAME}' tool but 'docs_config' is 'None'. "
328
+ f"Provide a valid 'docs_config' to register this tool."
329
+ )
462
330
 
463
331
  # Validate tool names if provided
464
332
  if tool_names is not None:
@@ -469,30 +337,35 @@ def register_tools(
469
337
  sorted_all = sorted(all_tools)
470
338
  raise ValueError(f"Unknown tools: {', '.join(sorted_invalid)}\nAvailable tools: {', '.join(sorted_all)}")
471
339
 
472
- # Warn if search_docs was requested but config is missing
473
- if "search_docs" in tool_names and not docs_available:
474
- logging.warning(
475
- "Documentation search tool requested but not available. To enable, set the DEEPSET_DOCS_SHARE_URL "
476
- "environment variable."
477
- )
478
-
479
340
  tools_to_register = tool_names.copy()
480
341
  else:
481
342
  tools_to_register = set(TOOL_REGISTRY.keys())
482
343
 
483
- # Warn if search_docs would be skipped in "all tools" mode
484
- if not docs_available:
485
- logging.warning(
486
- "Documentation search tool not enabled. To enable, set the DEEPSET_DOCS_SHARE_URL environment variable."
487
- )
488
-
489
- # Remove search_docs if config is not available
490
- if not docs_available:
491
- tools_to_register.discard("search_docs")
492
-
493
344
  for tool_name in tools_to_register:
494
345
  base_func, config = TOOL_REGISTRY[tool_name]
495
- # Create enhanced tool
496
- enhanced_tool = create_enhanced_tool(base_func, config, workspace_mode, workspace)
497
346
 
498
- mcp.add_tool(enhanced_tool, name=tool_name, structured_output=False)
347
+ if tool_name == DOCS_SEARCH_TOOL_NAME:
348
+ # search_docs is a special tool.
349
+ # base_func is a factory function.
350
+ # We configure with the docs_config to get the actual tool function.
351
+ enhanced_tool = base_func(config=docs_config)
352
+ elif tool_name in ("get_from_object_store", "get_slice_from_object_store"):
353
+ # ObjectStore tools are factory functions that need an explorer created from the store
354
+ if object_store is None:
355
+ raise ValueError(f"ObjectStore instance is required for {tool_name}")
356
+
357
+ explorer = RichExplorer(store=object_store)
358
+ enhanced_tool = base_func(explorer=explorer)
359
+ else:
360
+ enhanced_tool = build_tool(
361
+ base_func=base_func,
362
+ config=config,
363
+ workspace_mode=workspace_mode,
364
+ workspace=workspace,
365
+ use_request_context=get_api_key_from_authorization_header,
366
+ base_url=base_url,
367
+ object_store=object_store,
368
+ api_key=api_key,
369
+ )
370
+
371
+ mcp_server_instance.add_tool(enhanced_tool, name=tool_name)