mcp-hydrolix 0.1.3__py3-none-any.whl → 0.1.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.
- mcp_hydrolix/auth/__init__.py +29 -0
- mcp_hydrolix/auth/credentials.py +63 -0
- mcp_hydrolix/auth/mcp_providers.py +137 -0
- mcp_hydrolix/log/__init__.py +6 -0
- mcp_hydrolix/log/log.py +60 -0
- mcp_hydrolix/log/log.yaml +40 -0
- mcp_hydrolix/log/utils.py +56 -0
- mcp_hydrolix/main.py +69 -1
- mcp_hydrolix/mcp_env.py +203 -22
- mcp_hydrolix/mcp_server.py +256 -164
- mcp_hydrolix/utils.py +70 -0
- mcp_hydrolix-0.1.5.dist-info/METADATA +314 -0
- mcp_hydrolix-0.1.5.dist-info/RECORD +17 -0
- {mcp_hydrolix-0.1.3.dist-info → mcp_hydrolix-0.1.5.dist-info}/WHEEL +1 -1
- {mcp_hydrolix-0.1.3.dist-info → mcp_hydrolix-0.1.5.dist-info}/licenses/LICENSE +1 -1
- mcp_hydrolix-0.1.3.dist-info/METADATA +0 -146
- mcp_hydrolix-0.1.3.dist-info/RECORD +0 -9
- {mcp_hydrolix-0.1.3.dist-info → mcp_hydrolix-0.1.5.dist-info}/entry_points.txt +0 -0
mcp_hydrolix/mcp_env.py
CHANGED
|
@@ -4,10 +4,26 @@ This module handles all environment variable configuration with sensible default
|
|
|
4
4
|
and type conversion.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from dataclasses import dataclass
|
|
8
7
|
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
12
|
+
from .auth.credentials import HydrolixCredential, ServiceAccountToken, UsernamePassword
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TransportType(str, Enum):
|
|
16
|
+
"""Supported MCP server transport types."""
|
|
17
|
+
|
|
18
|
+
STDIO = "stdio"
|
|
19
|
+
HTTP = "http"
|
|
20
|
+
SSE = "sse"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def values(cls) -> list[str]:
|
|
24
|
+
"""Get all valid transport values."""
|
|
25
|
+
return [transport.value for transport in cls]
|
|
26
|
+
|
|
11
27
|
|
|
12
28
|
@dataclass
|
|
13
29
|
class HydrolixConfig:
|
|
@@ -18,24 +34,63 @@ class HydrolixConfig:
|
|
|
18
34
|
|
|
19
35
|
Required environment variables:
|
|
20
36
|
HYDROLIX_HOST: The hostname of the Hydrolix server
|
|
21
|
-
HYDROLIX_USER: The username for authentication
|
|
22
|
-
HYDROLIX_PASSWORD: The password for authentication
|
|
23
37
|
|
|
24
38
|
Optional environment variables (with defaults):
|
|
39
|
+
HYDROLIX_TOKEN: Service account token to the Hydrolix Server (this or user+password is required)
|
|
40
|
+
HYDROLIX_USER: The username for authentication (this or token is required)
|
|
41
|
+
HYDROLIX_PASSWORD: The password for authentication (this or token is required)
|
|
25
42
|
HYDROLIX_PORT: The port number (default: 8088)
|
|
26
43
|
HYDROLIX_VERIFY: Verify SSL certificates (default: true)
|
|
27
44
|
HYDROLIX_CONNECT_TIMEOUT: Connection timeout in seconds (default: 30)
|
|
28
45
|
HYDROLIX_SEND_RECEIVE_TIMEOUT: Send/receive timeout in seconds (default: 300)
|
|
29
46
|
HYDROLIX_DATABASE: Default database to use (default: None)
|
|
47
|
+
HYDROLIX_PROXY_PATH: Path to be added to the host URL. For instance, for servers behind an HTTP proxy (default: None)
|
|
48
|
+
HYDROLIX_MCP_SERVER_TRANSPORT: MCP server transport method - "stdio", "http", or "sse" (default: stdio)
|
|
49
|
+
HYDROLIX_MCP_BIND_HOST: Host to bind the MCP server to when using HTTP or SSE transport (default: 127.0.0.1)
|
|
50
|
+
HYDROLIX_MCP_BIND_PORT: Port to bind the MCP server to when using HTTP or SSE transport (default: 8000)
|
|
51
|
+
HYDROLIX_QUERIES_POOL_SIZE 100
|
|
52
|
+
HYDROLIX_MCP_REQUEST_TIMEOUT 120
|
|
53
|
+
HYDROLIX_MCP_WORKERS 3
|
|
54
|
+
HYDROLIX_MCP_WORKER_CONNECTIONS 200
|
|
55
|
+
HYDROLIX_MCP_MAX_REQUESTS 10000
|
|
56
|
+
HYDROLIX_MCP_MAX_REQUESTS_JITTER 1000
|
|
57
|
+
HYDROLIX_MCP_MAX_KEEPALIVE 10
|
|
30
58
|
"""
|
|
31
59
|
|
|
32
|
-
def __init__(self):
|
|
60
|
+
def __init__(self) -> None:
|
|
33
61
|
"""Initialize the configuration from environment variables."""
|
|
34
62
|
self._validate_required_vars()
|
|
63
|
+
# Credential to use for clickhouse connections when no per-request credential is provided
|
|
64
|
+
self._default_credential: Optional[HydrolixCredential] = None
|
|
65
|
+
|
|
66
|
+
# Set the default credential to the service account from the environment, if available
|
|
67
|
+
if (global_service_account := os.environ.get("HYDROLIX_TOKEN")) is not None:
|
|
68
|
+
self._default_credential = ServiceAccountToken(
|
|
69
|
+
global_service_account, f"https://{self.host}/config"
|
|
70
|
+
)
|
|
71
|
+
elif (global_username := os.environ.get("HYDROLIX_USER")) is not None and (
|
|
72
|
+
global_password := os.environ.get("HYDROLIX_PASSWORD")
|
|
73
|
+
) is not None:
|
|
74
|
+
# No global service account available. Set the default credential to the username/password
|
|
75
|
+
# from the environment, if available
|
|
76
|
+
self._default_credential = UsernamePassword(global_username, global_password)
|
|
77
|
+
|
|
78
|
+
def creds_with(self, request_credential: Optional[HydrolixCredential]) -> HydrolixCredential:
|
|
79
|
+
if request_credential is not None:
|
|
80
|
+
return request_credential
|
|
81
|
+
elif self._default_credential is not None:
|
|
82
|
+
return self._default_credential
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"No credentials available for Hydrolix connection. "
|
|
86
|
+
"Please provide credentials either through HYDROLIX_TOKEN or "
|
|
87
|
+
"HYDROLIX_USER/HYDROLIX_PASSWORD environment variables, "
|
|
88
|
+
"or pass credentials explicitly via the creds parameter."
|
|
89
|
+
)
|
|
35
90
|
|
|
36
91
|
@property
|
|
37
92
|
def host(self) -> str:
|
|
38
|
-
"""Get the Hydrolix host."""
|
|
93
|
+
"""Get the Hydrolix host. Called during __init__"""
|
|
39
94
|
return os.environ["HYDROLIX_HOST"]
|
|
40
95
|
|
|
41
96
|
@property
|
|
@@ -49,16 +104,6 @@ class HydrolixConfig:
|
|
|
49
104
|
return int(os.environ["HYDROLIX_PORT"])
|
|
50
105
|
return 8088
|
|
51
106
|
|
|
52
|
-
@property
|
|
53
|
-
def username(self) -> str:
|
|
54
|
-
"""Get the Hydrolix username."""
|
|
55
|
-
return os.environ["HYDROLIX_USER"]
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def password(self) -> str:
|
|
59
|
-
"""Get the Hydrolix password."""
|
|
60
|
-
return os.environ["HYDROLIX_PASSWORD"]
|
|
61
|
-
|
|
62
107
|
@property
|
|
63
108
|
def database(self) -> Optional[str]:
|
|
64
109
|
"""Get the default database name if set."""
|
|
@@ -72,6 +117,14 @@ class HydrolixConfig:
|
|
|
72
117
|
"""
|
|
73
118
|
return os.getenv("HYDROLIX_VERIFY", "true").lower() == "true"
|
|
74
119
|
|
|
120
|
+
@property
|
|
121
|
+
def secure(self) -> bool:
|
|
122
|
+
"""Get whether use secured connection.
|
|
123
|
+
|
|
124
|
+
Default: True
|
|
125
|
+
"""
|
|
126
|
+
return os.getenv("HYDROLIX_SECURE", "true").lower() == "true"
|
|
127
|
+
|
|
75
128
|
@property
|
|
76
129
|
def connect_timeout(self) -> int:
|
|
77
130
|
"""Get the connection timeout in seconds.
|
|
@@ -88,21 +141,136 @@ class HydrolixConfig:
|
|
|
88
141
|
"""
|
|
89
142
|
return int(os.getenv("HYDROLIX_SEND_RECEIVE_TIMEOUT", "300"))
|
|
90
143
|
|
|
91
|
-
|
|
92
|
-
|
|
144
|
+
@property
|
|
145
|
+
def query_pool_size(self) -> int:
|
|
146
|
+
"""Get the send/receive timeout in seconds.
|
|
147
|
+
|
|
148
|
+
Default: 300 (Hydrolix default)
|
|
149
|
+
"""
|
|
150
|
+
return int(os.getenv("HYDROLIX_QUERIES_POOL_SIZE", 100))
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def query_timeout_sec(self) -> int:
|
|
154
|
+
"""Get the send/receive timeout in seconds.
|
|
155
|
+
|
|
156
|
+
Default: 300 (Hydrolix default)
|
|
157
|
+
"""
|
|
158
|
+
return int(os.getenv("HYDROLIX_QUERY_TIMEOUT_SECS", 30))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def proxy_path(self) -> Optional[str]:
|
|
162
|
+
return os.getenv("HYDROLIX_PROXY_PATH")
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def mcp_server_transport(self) -> str:
|
|
166
|
+
"""Get the MCP server transport method.
|
|
167
|
+
|
|
168
|
+
Valid options: "stdio", "http", "sse"
|
|
169
|
+
Default: "stdio"
|
|
170
|
+
"""
|
|
171
|
+
transport = os.getenv("HYDROLIX_MCP_SERVER_TRANSPORT", TransportType.STDIO.value).lower()
|
|
172
|
+
|
|
173
|
+
# Validate transport type
|
|
174
|
+
if transport not in TransportType.values():
|
|
175
|
+
valid_options = ", ".join(f'"{t}"' for t in TransportType.values())
|
|
176
|
+
raise ValueError(f"Invalid transport '{transport}'. Valid options: {valid_options}")
|
|
177
|
+
return transport
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def mcp_bind_host(self) -> str:
|
|
181
|
+
"""Get the host to bind the MCP server to.
|
|
182
|
+
|
|
183
|
+
Only used when transport is "http" or "sse".
|
|
184
|
+
Default: "127.0.0.1"
|
|
185
|
+
"""
|
|
186
|
+
return os.getenv("HYDROLIX_MCP_BIND_HOST", "127.0.0.1")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def mcp_bind_port(self) -> int:
|
|
190
|
+
"""Get the port to bind the MCP server to.
|
|
191
|
+
|
|
192
|
+
Only used when transport is "http" or "sse".
|
|
193
|
+
Default: 8000
|
|
194
|
+
"""
|
|
195
|
+
return int(os.getenv("HYDROLIX_MCP_BIND_PORT", "8000"))
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def mcp_timeout(self) -> int:
|
|
199
|
+
"""Get the request timeout secunds.
|
|
200
|
+
|
|
201
|
+
Only used when transport is "http" or "sse".
|
|
202
|
+
Default: 120
|
|
203
|
+
"""
|
|
204
|
+
return int(os.getenv("HYDROLIX_MCP_REQUEST_TIMEOUT", 120))
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def mcp_workers(self) -> int:
|
|
208
|
+
"""Get the number of worker processes.
|
|
209
|
+
|
|
210
|
+
Only used when transport is "http" or "sse".
|
|
211
|
+
Default: 1
|
|
212
|
+
"""
|
|
213
|
+
return int(os.getenv("HYDROLIX_MCP_WORKERS", 1))
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def mcp_worker_connections(self) -> int:
|
|
217
|
+
"""Get the max number of concurrent requests per worker.
|
|
218
|
+
|
|
219
|
+
Only used when transport is "http" or "sse".
|
|
220
|
+
Default: 200
|
|
221
|
+
"""
|
|
222
|
+
return int(os.getenv("HYDROLIX_MCP_WORKER_CONNECTIONS", 100))
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def mcp_max_requests_jitter(self) -> int:
|
|
226
|
+
"""Get the random parameter to randomize time process is reloaded after max_requests.
|
|
227
|
+
|
|
228
|
+
Only used when transport is "http" or "sse".
|
|
229
|
+
Default: 10000
|
|
230
|
+
"""
|
|
231
|
+
return int(os.getenv("HYDROLIX_MCP_MAX_REQUESTS_JITTER", 1000))
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def mcp_max_requests(self) -> int:
|
|
235
|
+
"""Get the max number of requests handled by worker before it is restarted.
|
|
236
|
+
|
|
237
|
+
Only used when transport is "http" or "sse".
|
|
238
|
+
Default: 1000
|
|
239
|
+
"""
|
|
240
|
+
return int(os.getenv("HYDROLIX_MCP_MAX_REQUESTS", 10000))
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def mcp_keepalive(self) -> int:
|
|
244
|
+
"""Get a seconds of idle keepalive connections are kept alive.
|
|
245
|
+
|
|
246
|
+
Only used when transport is "http" or "sse".
|
|
247
|
+
Default: 10
|
|
248
|
+
"""
|
|
249
|
+
return int(os.getenv("HYDROLIX_MCP_MAX_KEEPALIVE", 10))
|
|
250
|
+
|
|
251
|
+
def get_client_config(self, request_credential: Optional[HydrolixCredential]) -> dict:
|
|
252
|
+
"""
|
|
253
|
+
Get the configuration dictionary for clickhouse_connect client.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
request_credential: Optional credentials to use for this request. If not provided,
|
|
257
|
+
falls back to the default credential for this HydrolixConfig
|
|
93
258
|
|
|
94
259
|
Returns:
|
|
95
260
|
dict: Configuration ready to be passed to clickhouse_connect.get_client()
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If no credentials could be inferred for the request (either from
|
|
264
|
+
the startup environment or provided in the request)
|
|
96
265
|
"""
|
|
97
266
|
config = {
|
|
98
267
|
"host": self.host,
|
|
99
268
|
"port": self.port,
|
|
100
|
-
"
|
|
101
|
-
"password": self.password,
|
|
102
|
-
"secure": True,
|
|
269
|
+
"secure": self.secure,
|
|
103
270
|
"verify": self.verify,
|
|
104
271
|
"connect_timeout": self.connect_timeout,
|
|
105
272
|
"send_receive_timeout": self.send_receive_timeout,
|
|
273
|
+
"executor_threads": self.query_pool_size,
|
|
106
274
|
"client_name": "mcp_hydrolix",
|
|
107
275
|
}
|
|
108
276
|
|
|
@@ -110,19 +278,32 @@ class HydrolixConfig:
|
|
|
110
278
|
if self.database:
|
|
111
279
|
config["database"] = self.database
|
|
112
280
|
|
|
281
|
+
if self.proxy_path:
|
|
282
|
+
config["proxy_path"] = self.proxy_path
|
|
283
|
+
|
|
284
|
+
# Add credentials
|
|
285
|
+
config |= self.creds_with(request_credential).clickhouse_config_entries()
|
|
286
|
+
|
|
113
287
|
return config
|
|
114
288
|
|
|
115
289
|
def _validate_required_vars(self) -> None:
|
|
116
|
-
"""Validate that all required environment variables are set.
|
|
290
|
+
"""Validate that all required environment variables are set. Called during __init__.
|
|
117
291
|
|
|
118
292
|
Raises:
|
|
119
293
|
ValueError: If any required environment variable is missing.
|
|
120
294
|
"""
|
|
121
295
|
missing_vars = []
|
|
122
|
-
|
|
296
|
+
required_vars = ["HYDROLIX_HOST"]
|
|
297
|
+
for var in required_vars:
|
|
123
298
|
if var not in os.environ:
|
|
124
299
|
missing_vars.append(var)
|
|
125
300
|
|
|
301
|
+
# HYDROLIX_USER and HYDROLIX_PASSWORD must either be both present or both absent
|
|
302
|
+
if ("HYDROLIX_USER" in os.environ) != ("HYDROLIX_PASSWORD" in os.environ):
|
|
303
|
+
raise ValueError(
|
|
304
|
+
"User/password authentication is only partially configured: set both HYDROLIX_USER and HYDROLIX_PASSWORD"
|
|
305
|
+
)
|
|
306
|
+
|
|
126
307
|
if missing_vars:
|
|
127
308
|
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
|
|
128
309
|
|