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.
deepset_mcp/__init__.py CHANGED
@@ -2,9 +2,9 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import importlib.metadata
5
+ from deepset_mcp.config import DEEPSET_DOCS_DEFAULT_SHARE_URL
6
+ from deepset_mcp.server import configure_mcp_server
7
+ from deepset_mcp.tool_models import WorkspaceMode
8
+ from deepset_mcp.tool_registry import ALL_DEEPSET_TOOLS
6
9
 
7
- try:
8
- __version__ = importlib.metadata.version(__name__)
9
- except importlib.metadata.PackageNotFoundError:
10
- __version__ = "0.0.0" # Fallback for development mode
10
+ __all__ = ["configure_mcp_server", "WorkspaceMode", "ALL_DEEPSET_TOOLS", "DEEPSET_DOCS_DEFAULT_SHARE_URL"]
@@ -6,6 +6,7 @@ import json
6
6
  import time
7
7
  from collections.abc import AsyncIterator
8
8
  from contextlib import AbstractAsyncContextManager, asynccontextmanager
9
+ from copy import deepcopy
9
10
  from dataclasses import dataclass
10
11
  from typing import Any, Generic, Literal, Protocol, TypeVar, cast, overload
11
12
 
@@ -198,8 +199,10 @@ class AsyncTransport:
198
199
  config : dict, optional
199
200
  Configuration for httpx.AsyncClient, e.g., {'timeout': 10.0}
200
201
  """
201
- config = config or {}
202
- # Ensure auth header
202
+ # We deepcopy the config so that we don't mutate it when used for subsequent initializations
203
+ config = deepcopy(config) or {}
204
+
205
+ # Merge auth and other config headers
203
206
  headers = config.pop("headers", {})
204
207
  headers.setdefault("Authorization", f"Bearer {api_key}")
205
208
  # Build client kwargs
deepset_mcp/config.py CHANGED
@@ -4,7 +4,14 @@
4
4
 
5
5
  """This module contains static configuration for the deepset MCP server."""
6
6
 
7
- from deepset_mcp import __version__
7
+ import importlib.metadata
8
+
9
+ try:
10
+ __version__ = importlib.metadata.version("deepset-mcp")
11
+ except importlib.metadata.PackageNotFoundError:
12
+ __version__ = "0.0.0" # Fallback for development mode
13
+
14
+ PACKAGE_VERSION = __version__
8
15
 
9
16
  # We need this mapping to which environment variables integrations are mapped to
10
17
  # The mapping is maintained in the pipeline operator:
@@ -29,5 +36,6 @@ TOKEN_DOMAIN_MAPPING = {
29
36
  }
30
37
 
31
38
  DEEPSET_DOCS_DEFAULT_SHARE_URL = "https://cloud.deepset.ai/shared_prototypes?share_token=prototype_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODM0MjE0OTguNTk5LCJhdWQiOiJleHRlcm5hbCB1c2VyIiwiaXNzIjoiZEMiLCJ3b3Jrc3BhY2VfaWQiOiI4YzI0ZjExMi1iMjljLTQ5MWMtOTkzOS1hZTkxMDRhNTQyMWMiLCJ3b3Jrc3BhY2VfbmFtZSI6ImRjLWRvY3MtY29udGVudCIsIm9yZ2FuaXphdGlvbl9pZCI6ImNhOWYxNGQ0LWMyYzktNDYwZC04ZDI2LWY4Y2IwYWNhMDI0ZiIsInNoYXJlX2lkIjoiY2Y3MTA3ODAtOThmNi00MzlmLThiNzYtMmMwNDkyODNiMDZhIiwibG9naW5fcmVxdWlyZWQiOmZhbHNlfQ.5j6DCNRQ1_KB8lhIJqHyw2hBIleEW1_Y_UBuH6MTYY0"
39
+ DOCS_SEARCH_TOOL_NAME = "search_docs"
32
40
 
33
41
  DEFAULT_CLIENT_HEADER = {"headers": {"User-Agent": f"deepset-mcp/{__version__}"}}
deepset_mcp/main.py CHANGED
@@ -2,191 +2,208 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import argparse
6
- import asyncio
7
5
  import logging
8
6
  import os
9
- from pathlib import Path
10
- from urllib.parse import parse_qs, urlparse
7
+ from enum import StrEnum
8
+ from typing import Annotated
11
9
 
12
- import jwt
10
+ import typer
13
11
  from mcp.server.fastmcp import FastMCP
14
12
 
15
- from deepset_mcp.api.client import AsyncDeepsetClient
16
- from deepset_mcp.config import DEEPSET_DOCS_DEFAULT_SHARE_URL
17
- from deepset_mcp.tool_factory import WorkspaceMode, register_tools
13
+ from deepset_mcp.config import DEEPSET_DOCS_DEFAULT_SHARE_URL, DOCS_SEARCH_TOOL_NAME
14
+ from deepset_mcp.server import configure_mcp_server
15
+ from deepset_mcp.tool_models import WorkspaceMode
16
+ from deepset_mcp.tool_registry import TOOL_REGISTRY
18
17
 
19
- # Initialize MCP Server
20
- mcp = FastMCP("Deepset Cloud MCP", settings={"log_level": "ERROR"})
21
18
 
22
- logging.getLogger("uvicorn").setLevel(logging.WARNING)
23
- logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
24
- logging.getLogger("fastapi").setLevel(logging.WARNING)
25
- logging.getLogger("httpx").setLevel(logging.WARNING)
26
- logging.getLogger("httpcore").setLevel(logging.WARNING)
27
- logging.getLogger("mcp").setLevel(logging.WARNING)
19
+ class TransportEnum(StrEnum):
20
+ """Transport mode for the MCP server."""
28
21
 
22
+ STDIO = "stdio"
23
+ STREAMABLE_HTTP = "streamable-http"
29
24
 
30
- @mcp.prompt()
31
- async def deepset_copilot() -> str:
32
- """System prompt for the deepset copilot."""
33
- prompt_path = Path(__file__).parent / "prompts/deepset_copilot_prompt.md"
34
25
 
35
- return prompt_path.read_text()
26
+ app = typer.Typer(
27
+ name="deepset-mcp",
28
+ help="Run the Deepset MCP server to interact with the deepset AI platform.",
29
+ no_args_is_help=True,
30
+ )
36
31
 
37
32
 
38
- @mcp.prompt()
39
- async def deepset_recommended_prompt() -> str:
40
- """Recommended system prompt for the deepset copilot."""
41
- prompt_path = Path(__file__).parent / "prompts/deepset_debugging_agent.md"
42
-
43
- return prompt_path.read_text()
44
-
45
-
46
- async def fetch_shared_prototype_details(share_url: str) -> tuple[str, str, str]:
47
- """Gets the pipeline name, workspace name and an API token for a shared prototype url.
48
-
49
- :param share_url: The URL of a shared prototype on the deepset platform.
50
-
51
- :returns: A tuple containing the pipeline name, workspace name and an API token.
52
- """
53
- parsed_url = urlparse(share_url)
54
- query_params = parse_qs(parsed_url.query)
55
- share_token = query_params.get("share_token", [None])[0]
56
- if not share_token:
57
- raise ValueError("Invalid share URL: missing share_token parameter.")
58
-
59
- jwt_token = share_token.replace("prototype_", "")
60
-
61
- decoded_token = jwt.decode(jwt_token, options={"verify_signature": False})
62
- workspace_name = decoded_token.get("workspace_name")
63
- if not workspace_name:
64
- raise ValueError("Invalid JWT in share_token: missing 'workspace_name'.")
65
-
66
- share_id = decoded_token.get("share_id")
67
- if not share_id:
68
- raise ValueError("Invalid JWT in share_token: missing 'share_id'.")
69
-
70
- # For shared prototypes, we need to:
71
- # 1. Fetch prototype details (pipeline name) using the information encoded in the JWT
72
- # 2. Create a shared prototype user
73
- async with AsyncDeepsetClient(api_key=share_token) as client:
74
- response = await client.request(f"/v1/workspaces/{workspace_name}/shared_prototypes/{share_id}")
75
- if not response.success:
76
- raise ValueError(f"Failed to fetch shared prototype details: {response.status_code} {response.json}")
77
-
78
- data = response.json or {}
79
- pipeline_names: list[str] = data.get("pipeline_names", [])
80
- if not pipeline_names:
81
- raise ValueError("No pipeline names found in shared prototype response.")
82
-
83
- user_info = await client.request("/v1/workspaces/dc-docs-content/shared_prototype_users", method="POST")
84
-
85
- if not user_info.success:
86
- raise ValueError("Failed to fetch user information from shared prototype response.")
87
-
88
- user_data = user_info.json or {}
89
-
90
- try:
91
- api_key = user_data["user_token"]
92
- except KeyError:
93
- raise ValueError("No user token in shared prototype response.") from None
94
-
95
- return workspace_name, pipeline_names[0], api_key
96
-
97
-
98
- def main() -> None:
99
- """Entrypoint for the deepset MCP server."""
100
- parser = argparse.ArgumentParser(description="Run the Deepset MCP server.")
101
- parser.add_argument(
102
- "--workspace",
103
- "-w",
104
- help="Deepset workspace (env DEEPSET_WORKSPACE)",
105
- )
106
- parser.add_argument(
107
- "--api-key",
108
- "-k",
109
- help="Deepset API key (env DEEPSET_API_KEY)",
110
- )
111
- parser.add_argument(
112
- "--docs-share-url",
113
- "-d",
114
- default=DEEPSET_DOCS_DEFAULT_SHARE_URL,
115
- help="Deepset docs search share URL (env DEEPSET_DOCS_SHARE_URL)",
116
- )
117
- parser.add_argument(
118
- "--workspace-mode",
119
- "-m",
120
- choices=[WorkspaceMode.STATIC, WorkspaceMode.DYNAMIC],
121
- default=WorkspaceMode.STATIC,
122
- help=(
123
- "Whether workspace should be set statically or dynamically provided during a tool call. "
124
- f"Default: '{WorkspaceMode.STATIC}'"
33
+ @app.command()
34
+ def main(
35
+ workspace: Annotated[
36
+ str | None,
37
+ typer.Option(
38
+ "--workspace",
39
+ help="Deepset workspace name. Can also be set via DEEPSET_WORKSPACE environment variable.",
125
40
  ),
126
- )
127
- parser.add_argument(
128
- "--tools",
129
- "-t",
130
- nargs="*",
131
- help="Space-separated list of tools to register (default: all)",
132
- )
133
- parser.add_argument(
134
- "--list-tools",
135
- "-l",
136
- action="store_true",
137
- help="List all available tools and exit",
138
- )
139
- args = parser.parse_args()
140
-
41
+ ] = None,
42
+ api_key: Annotated[
43
+ str | None,
44
+ typer.Option(
45
+ "--api-key",
46
+ help="Deepset API key for authentication. Can also be set via DEEPSET_API_KEY environment variable.",
47
+ ),
48
+ ] = None,
49
+ api_url: Annotated[
50
+ str | None,
51
+ typer.Option(
52
+ "--api-url",
53
+ help="Deepset API base URL. Can also be set via DEEPSET_API_URL environment variable.",
54
+ ),
55
+ ] = None,
56
+ docs_share_url: Annotated[
57
+ str | None,
58
+ typer.Option(
59
+ "--docs-share-url",
60
+ help="Deepset docs search share URL. Can also be set via DEEPSET_DOCS_SHARE_URL environment variable.",
61
+ ),
62
+ ] = None,
63
+ workspace_mode: Annotated[
64
+ WorkspaceMode,
65
+ typer.Option(
66
+ "--workspace-mode",
67
+ help="Whether workspace should be set statically or dynamically provided during a tool call.",
68
+ ),
69
+ ] = WorkspaceMode.STATIC,
70
+ tools: Annotated[
71
+ list[str] | None,
72
+ typer.Option(
73
+ "--tools",
74
+ help="Space-separated list of tools to register. If not specified, all available tools will be registered.",
75
+ ),
76
+ ] = None,
77
+ list_tools: Annotated[
78
+ bool,
79
+ typer.Option(
80
+ "--list-tools",
81
+ help="List all available tools and exit.",
82
+ ),
83
+ ] = False,
84
+ api_key_from_auth_header: Annotated[
85
+ bool,
86
+ typer.Option(
87
+ "--api-key-from-auth-header/--no-api-key-from-auth-header",
88
+ help="Get the deepset API key from the request's authorization header instead of using a static key.",
89
+ ),
90
+ ] = False,
91
+ transport: Annotated[
92
+ TransportEnum,
93
+ typer.Option(
94
+ "--transport",
95
+ help="The type of transport to use for running the MCP server.",
96
+ ),
97
+ ] = TransportEnum.STDIO,
98
+ object_store_backend: Annotated[
99
+ str | None,
100
+ typer.Option(
101
+ "--object-store-backend",
102
+ help="Object store backend type: 'memory' or 'redis'. "
103
+ "Can also be set via OBJECT_STORE_BACKEND environment variable. Default is 'memory'.",
104
+ ),
105
+ ] = None,
106
+ object_store_redis_url: Annotated[
107
+ str | None,
108
+ typer.Option(
109
+ "--object-store-redis-url",
110
+ help="Redis connection URL (e.g., redis://localhost:6379). "
111
+ "Can also be set via OBJECT_STORE_REDIS_URL environment variable.",
112
+ ),
113
+ ] = None,
114
+ object_store_ttl: Annotated[
115
+ int,
116
+ typer.Option(
117
+ "--object-store-ttl",
118
+ help="TTL in seconds for stored objects. Default: 600 (10 minutes). "
119
+ "Can also be set via OBJECT_STORE_TTL environment variable.",
120
+ ),
121
+ ] = 600,
122
+ ) -> None:
123
+ """
124
+ Run the Deepset MCP server.
125
+
126
+ The Deepset MCP server provides tools to interact with the deepset AI platform,
127
+ allowing you to create, debug, and learn about pipelines on the platform.
128
+
129
+ :param workspace: Deepset workspace name
130
+ :param api_key: Deepset API key for authentication
131
+ :param api_url: Deepset API base URL
132
+ :param docs_share_url: Deepset docs search share URL
133
+ :param workspace_mode: Whether workspace should be set statically or dynamically
134
+ :param tools: List of tools to register
135
+ :param list_tools: List all available tools and exit
136
+ :param api_key_from_auth_header: Get API key from authorization header
137
+ :param transport: Type of transport to use for the MCP server
138
+ :param object_store_backend: Object store backend type ('memory' or 'redis')
139
+ :param object_store_redis_url: Redis connection URL (required if backend='redis')
140
+ :param object_store_ttl: TTL in seconds for stored objects
141
+ """
141
142
  # Handle --list-tools flag early
142
- if args.list_tools:
143
- from deepset_mcp.tool_factory import TOOL_REGISTRY
144
-
145
- print("Available tools:")
143
+ if list_tools:
144
+ typer.echo("Available tools:")
146
145
  for tool_name in sorted(TOOL_REGISTRY.keys()):
147
- print(f" {tool_name}")
148
- return
149
-
150
- # prefer flags, fallback to env
151
- workspace = args.workspace or os.getenv("DEEPSET_WORKSPACE")
152
- api_key = args.api_key or os.getenv("DEEPSET_API_KEY")
153
- docs_share_url = args.docs_share_url or os.getenv("DEEPSET_DOCS_SHARE_URL")
154
-
155
- if docs_share_url:
156
- try:
157
- workspace_name, pipeline_name, api_key_docs = asyncio.run(fetch_shared_prototype_details(docs_share_url))
158
- os.environ["DEEPSET_DOCS_WORKSPACE"] = workspace_name
159
- os.environ["DEEPSET_DOCS_PIPELINE_NAME"] = pipeline_name
160
- os.environ["DEEPSET_DOCS_API_KEY"] = api_key_docs
161
- except (ValueError, jwt.DecodeError) as e:
162
- parser.error(f"Error processing --docs-share-url: {e}")
163
-
164
- # Create server configuration
165
- workspace_mode = WorkspaceMode(args.workspace_mode)
166
-
167
- if workspace_mode == WorkspaceMode.STATIC:
168
- if not workspace:
169
- parser.error("Missing workspace: set --workspace or DEEPSET_WORKSPACE")
170
-
171
- if not api_key:
172
- parser.error("Missing API key: set --api-key or DEEPSET_API_KEY")
173
-
174
- # make sure downstream tools see them (for implicit mode)
175
- if workspace:
176
- os.environ["DEEPSET_WORKSPACE"] = workspace
177
- os.environ["DEEPSET_API_KEY"] = api_key
178
-
179
- # Parse tool names if provided
180
- tool_names = None
181
- if args.tools:
182
- tool_names = set(args.tools)
183
-
184
- # Register tools based on configuration
185
- register_tools(mcp, workspace_mode, workspace, tool_names)
146
+ typer.echo(f" {tool_name}")
147
+ raise typer.Exit()
148
+
149
+ # Prefer command line arguments, fallback to environment variables
150
+ workspace = workspace or os.getenv("DEEPSET_WORKSPACE")
151
+ api_key = api_key or os.getenv("DEEPSET_API_KEY")
152
+ api_url = api_url or os.getenv("DEEPSET_API_URL")
153
+ docs_share_url = docs_share_url or os.getenv("DEEPSET_DOCS_SHARE_URL", DEEPSET_DOCS_DEFAULT_SHARE_URL)
154
+
155
+ # ObjectStore configuration
156
+ backend = str(object_store_backend or os.getenv("OBJECT_STORE_BACKEND", "memory"))
157
+ redis_url = object_store_redis_url or os.getenv("OBJECT_STORE_REDIS_URL")
158
+ ttl = int(os.getenv("OBJECT_STORE_TTL", str(object_store_ttl)))
159
+
160
+ if tools:
161
+ tool_names = set(tools)
162
+ else:
163
+ logging.info("Registering all available tools.")
164
+ tool_names = set(TOOL_REGISTRY.keys())
165
+
166
+ if api_key is None and not api_key_from_auth_header:
167
+ typer.echo(
168
+ "Error: API key is required. Either provide --api-key or use --api-key-from-auth-header "
169
+ "to fetch the API key from the authorization header.",
170
+ err=True,
171
+ )
172
+ raise typer.Exit(1)
173
+
174
+ if workspace_mode == WorkspaceMode.STATIC and not workspace:
175
+ typer.echo(
176
+ "Error: Workspace is required when using static workspace mode. "
177
+ "Set --workspace or DEEPSET_WORKSPACE environment variable.",
178
+ err=True,
179
+ )
180
+ raise typer.Exit(1)
181
+
182
+ if DOCS_SEARCH_TOOL_NAME in tool_names and docs_share_url is None:
183
+ typer.echo(
184
+ f"Error: {DOCS_SEARCH_TOOL_NAME} tool is requested but no docs share URL provided. "
185
+ "Set --docs-share-url or DEEPSET_DOCS_SHARE_URL environment variable.",
186
+ err=True,
187
+ )
188
+ raise typer.Exit(1)
189
+
190
+ mcp = FastMCP("deepset AI platform MCP server")
191
+ configure_mcp_server(
192
+ mcp_server_instance=mcp,
193
+ workspace_mode=workspace_mode,
194
+ deepset_api_key=api_key,
195
+ deepset_api_url=api_url,
196
+ deepset_workspace=workspace,
197
+ tools_to_register=tool_names,
198
+ deepset_docs_shareable_prototype_url=docs_share_url,
199
+ get_api_key_from_authorization_header=api_key_from_auth_header,
200
+ object_store_backend=backend,
201
+ object_store_redis_url=redis_url,
202
+ object_store_ttl=ttl,
203
+ )
186
204
 
187
- # run with SSE transport (HTTP+Server-Sent Events)
188
- mcp.run(transport="stdio")
205
+ mcp.run(transport=transport.value)
189
206
 
190
207
 
191
208
  if __name__ == "__main__":
192
- main()
209
+ app()
deepset_mcp/py.typed ADDED
File without changes
deepset_mcp/server.py ADDED
@@ -0,0 +1,154 @@
1
+ # SPDX-FileCopyrightText: 2025-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import asyncio
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ import jwt
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from deepset_mcp.api.client import AsyncDeepsetClient
12
+ from deepset_mcp.config import DEEPSET_DOCS_DEFAULT_SHARE_URL
13
+ from deepset_mcp.store import initialize_store
14
+ from deepset_mcp.tool_factory import register_tools
15
+ from deepset_mcp.tool_models import DeepsetDocsConfig, WorkspaceMode
16
+ from deepset_mcp.tool_registry import TOOL_REGISTRY
17
+
18
+
19
+ def configure_mcp_server(
20
+ mcp_server_instance: FastMCP,
21
+ workspace_mode: WorkspaceMode | str,
22
+ tools_to_register: set[str] | None = None,
23
+ deepset_api_key: str | None = None,
24
+ deepset_api_url: str | None = None,
25
+ deepset_workspace: str | None = None,
26
+ deepset_docs_shareable_prototype_url: str | None = None,
27
+ get_api_key_from_authorization_header: bool = False,
28
+ object_store_backend: str = "memory",
29
+ object_store_redis_url: str | None = None,
30
+ object_store_ttl: int = 600,
31
+ ) -> None:
32
+ """Configure the MCP server with the specified tools and settings.
33
+
34
+ :param mcp_server_instance: The FastMCP server instance to configure
35
+ :param workspace_mode: The workspace mode ("static" or "dynamic")
36
+ :param tools_to_register: Set of tool names to register with the server.
37
+ Will register all tools if set to None.
38
+ :param deepset_api_key: Optional Deepset API key for authentication
39
+ :param deepset_api_url: Optional Deepset API base URL
40
+ :param deepset_workspace: Optional workspace name for static mode
41
+ :param deepset_docs_shareable_prototype_url: Shareable prototype URL that allows access to a docs search pipeline.
42
+ Will fall back to the default shareable prototype URL if set to None.
43
+ :param get_api_key_from_authorization_header: Whether to extract API key from authorization header
44
+ :param object_store_backend: Object store backend type ('memory' or 'redis')
45
+ :param object_store_redis_url: Redis connection URL (required if backend='redis')
46
+ :param object_store_ttl: TTL in seconds for stored objects
47
+ :raises ValueError: If required parameters are missing or invalid
48
+ """
49
+ if tools_to_register is None:
50
+ tools_to_register = set(TOOL_REGISTRY.keys())
51
+
52
+ if deepset_docs_shareable_prototype_url is None:
53
+ deepset_docs_shareable_prototype_url = DEEPSET_DOCS_DEFAULT_SHARE_URL
54
+
55
+ if isinstance(workspace_mode, str):
56
+ workspace_mode = WorkspaceMode(workspace_mode)
57
+
58
+ if workspace_mode == WorkspaceMode.STATIC and deepset_workspace is None:
59
+ raise ValueError(
60
+ "Static workspace mode requires a workspace name. "
61
+ "Please provide 'deepset_workspace' when using WorkspaceMode.STATIC."
62
+ )
63
+
64
+ if deepset_api_key is None and not get_api_key_from_authorization_header:
65
+ raise ValueError(
66
+ "API key is required for authentication. "
67
+ "Please provide 'deepset_api_key' or enable 'get_api_key_from_authorization_header'."
68
+ )
69
+
70
+ workspace_name, pipeline_name, api_key_docs = asyncio.run(
71
+ fetch_shared_prototype_details(deepset_docs_shareable_prototype_url)
72
+ )
73
+ docs_config = DeepsetDocsConfig(api_key=api_key_docs, workspace_name=workspace_name, pipeline_name=pipeline_name)
74
+
75
+ # Initialize the store before registering tools
76
+ store = initialize_store(backend=object_store_backend, redis_url=object_store_redis_url, ttl=object_store_ttl)
77
+
78
+ register_tools(
79
+ mcp_server_instance=mcp_server_instance,
80
+ workspace_mode=workspace_mode,
81
+ workspace=deepset_workspace,
82
+ tool_names=tools_to_register,
83
+ docs_config=docs_config,
84
+ get_api_key_from_authorization_header=get_api_key_from_authorization_header,
85
+ api_key=deepset_api_key,
86
+ base_url=deepset_api_url,
87
+ object_store=store,
88
+ )
89
+
90
+
91
+ async def fetch_shared_prototype_details(share_url: str) -> tuple[str, str, str]:
92
+ """Extract pipeline name, workspace name and API token from a shared prototype URL.
93
+
94
+ :param share_url: The URL of a shared prototype on the Deepset platform
95
+ :returns: A tuple containing (workspace_name, pipeline_name, api_key)
96
+ :raises ValueError: If the URL is invalid or missing required parameters
97
+ """
98
+ parsed_url = urlparse(share_url)
99
+ query_params = parse_qs(parsed_url.query)
100
+ share_token = query_params.get("share_token", [None])[0]
101
+ if not share_token:
102
+ raise ValueError(
103
+ "Invalid share URL: missing 'share_token' parameter. Please provide a valid Deepset prototype share URL."
104
+ )
105
+
106
+ jwt_token = share_token.replace("prototype_", "")
107
+
108
+ decoded_token = jwt.decode(jwt_token, options={"verify_signature": False})
109
+ workspace_name = decoded_token.get("workspace_name")
110
+ if not workspace_name:
111
+ raise ValueError(
112
+ "Invalid share token: missing 'workspace_name' in JWT. The provided share URL may be corrupted or expired."
113
+ )
114
+
115
+ share_id = decoded_token.get("share_id")
116
+ if not share_id:
117
+ raise ValueError(
118
+ "Invalid share token: missing 'share_id' in JWT. The provided share URL may be corrupted or expired."
119
+ )
120
+
121
+ # For shared prototypes, we need to:
122
+ # 1. Fetch prototype details (pipeline name) using the information encoded in the JWT
123
+ # 2. Create a shared prototype user
124
+ async with AsyncDeepsetClient(api_key=share_token) as client:
125
+ response = await client.request(f"/v1/workspaces/{workspace_name}/shared_prototypes/{share_id}")
126
+ if not response.success:
127
+ raise ValueError(
128
+ f"Failed to fetch shared prototype details: HTTP {response.status_code}. Response: {response.json}"
129
+ )
130
+
131
+ data = response.json or {}
132
+ pipeline_names: list[str] = data.get("pipeline_names", [])
133
+ if not pipeline_names:
134
+ raise ValueError(
135
+ "No pipeline names found in shared prototype. The prototype may not be properly configured."
136
+ )
137
+
138
+ user_info = await client.request("/v1/workspaces/dc-docs-content/shared_prototype_users", method="POST")
139
+
140
+ if not user_info.success:
141
+ raise ValueError(
142
+ f"Failed to create user session for shared prototype. HTTP {user_info.status_code}: {user_info.json}"
143
+ )
144
+
145
+ user_data = user_info.json or {}
146
+
147
+ try:
148
+ api_key = user_data["user_token"]
149
+ except KeyError:
150
+ raise ValueError(
151
+ "No user token found in shared prototype response. Unable to authenticate with the prototype."
152
+ ) from None
153
+
154
+ return workspace_name, pipeline_names[0], api_key
deepset_mcp/store.py CHANGED
@@ -4,6 +4,55 @@
4
4
 
5
5
  """Global store for the MCP server."""
6
6
 
7
- from deepset_mcp.tools.tokonomics import ObjectStore
7
+ import functools
8
+ import logging
8
9
 
9
- STORE = ObjectStore()
10
+ from deepset_mcp.tools.tokonomics.object_store import InMemoryBackend, ObjectStore, ObjectStoreBackend, RedisBackend
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def create_redis_backend(url: str) -> ObjectStoreBackend:
16
+ """Create Redis backend, failing if connection fails.
17
+
18
+ :param url: Redis connection URL
19
+ :raises Exception: If Redis connection fails
20
+ """
21
+ try:
22
+ backend = RedisBackend(url)
23
+ logger.info(f"Successfully connected to Redis at {url} (using UUIDs for IDs)")
24
+
25
+ return backend
26
+
27
+ except Exception as e:
28
+ logger.error(f"Failed to connect to Redis at {url}: {e}")
29
+ raise
30
+
31
+
32
+ @functools.lru_cache(maxsize=1)
33
+ def initialize_store(
34
+ backend: str = "memory",
35
+ redis_url: str | None = None,
36
+ ttl: int = 600,
37
+ ) -> ObjectStore:
38
+ """Initialize the object store.
39
+
40
+ :param backend: Backend type ('memory' or 'redis')
41
+ :param redis_url: Redis connection URL (required if backend='redis')
42
+ :param ttl: Time-to-live in seconds for stored objects
43
+ :raises ValueError: If Redis backend selected but no URL provided
44
+ :raises Exception: If Redis connection fails
45
+ """
46
+ if backend == "redis":
47
+ if not redis_url:
48
+ raise ValueError("'redis_url' is None. Provide a 'redis_url' to use the redis backend.")
49
+ backend_instance = create_redis_backend(redis_url)
50
+
51
+ else:
52
+ logger.info("Using in-memory backend")
53
+ backend_instance = InMemoryBackend()
54
+
55
+ store = ObjectStore(backend=backend_instance, ttl=ttl)
56
+ logger.info(f"Initialized ObjectStore with {backend} backend and TTL={ttl}s")
57
+
58
+ return store