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/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
- def get_client_config(self) -> dict:
92
- """Get the configuration dictionary for clickhouse_connect client.
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
- "username": self.username,
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
- for var in ["HYDROLIX_HOST", "HYDROLIX_USER", "HYDROLIX_PASSWORD"]:
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