deepset-mcp 0.0.4__py3-none-any.whl → 0.0.5__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 +5 -5
- deepset_mcp/api/transport.py +5 -2
- deepset_mcp/config.py +9 -1
- deepset_mcp/main.py +186 -169
- deepset_mcp/py.typed +0 -0
- deepset_mcp/server.py +154 -0
- deepset_mcp/store.py +51 -2
- deepset_mcp/tool_factory.py +301 -428
- deepset_mcp/tool_models.py +42 -0
- deepset_mcp/tool_registry.py +208 -0
- deepset_mcp/tools/object_store.py +49 -0
- deepset_mcp/tools/tokonomics/__init__.py +2 -61
- deepset_mcp/tools/tokonomics/decorators.py +60 -87
- deepset_mcp/tools/tokonomics/explorer.py +32 -17
- deepset_mcp/tools/tokonomics/object_store.py +116 -139
- {deepset_mcp-0.0.4.dist-info → deepset_mcp-0.0.5.dist-info}/METADATA +59 -13
- {deepset_mcp-0.0.4.dist-info → deepset_mcp-0.0.5.dist-info}/RECORD +20 -15
- deepset_mcp-0.0.5.dist-info/entry_points.txt +2 -0
- deepset_mcp-0.0.4.dist-info/entry_points.txt +0 -2
- {deepset_mcp-0.0.4.dist-info → deepset_mcp-0.0.5.dist-info}/WHEEL +0 -0
- {deepset_mcp-0.0.4.dist-info → deepset_mcp-0.0.5.dist-info}/licenses/LICENSE +0 -0
deepset_mcp/__init__.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
import
|
|
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
|
-
|
|
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"]
|
deepset_mcp/api/transport.py
CHANGED
|
@@ -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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
from
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Annotated
|
|
11
9
|
|
|
12
|
-
import
|
|
10
|
+
import typer
|
|
13
11
|
from mcp.server.fastmcp import FastMCP
|
|
14
12
|
|
|
15
|
-
from deepset_mcp.
|
|
16
|
-
from deepset_mcp.
|
|
17
|
-
from deepset_mcp.
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
workspace =
|
|
152
|
-
api_key =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if workspace:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
mcp.run(transport="stdio")
|
|
205
|
+
mcp.run(transport=transport.value)
|
|
189
206
|
|
|
190
207
|
|
|
191
208
|
if __name__ == "__main__":
|
|
192
|
-
|
|
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
|
-
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
8
9
|
|
|
9
|
-
|
|
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
|