mcp-hydrolix 0.1.6__tar.gz → 0.1.7__tar.gz

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.
Files changed (35) hide show
  1. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/PKG-INFO +2 -1
  2. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/log.yaml +4 -0
  3. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/main.py +1 -0
  4. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/mcp_server.py +23 -3
  5. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/utils.py +4 -4
  6. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/pyproject.toml +2 -1
  7. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_utils.py +8 -15
  8. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/uv.lock +7 -5
  9. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.dockerignore +0 -0
  10. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.editorconfig +0 -0
  11. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/pull_request_template.md +0 -0
  12. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/workflows/publish.yml +0 -0
  13. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/workflows/tests.yaml +0 -0
  14. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.gitignore +0 -0
  15. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.pre-commit-config.yaml +0 -0
  16. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.python-version +0 -0
  17. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/Dockerfile +0 -0
  18. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/LICENSE +0 -0
  19. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/README.md +0 -0
  20. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/docker-compose.yaml +0 -0
  21. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/fastmcp.json +0 -0
  22. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/glama.json +0 -0
  23. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/__init__.py +0 -0
  24. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/__init__.py +0 -0
  25. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/credentials.py +0 -0
  26. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/mcp_providers.py +0 -0
  27. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/__init__.py +0 -0
  28. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/log.py +0 -0
  29. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/utils.py +0 -0
  30. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/mcp_env.py +0 -0
  31. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/test-services/docker-compose.yaml +0 -0
  32. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/__init__.py +0 -0
  33. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_log.py +0 -0
  34. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_mcp_server.py +0 -0
  35. {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_tool.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-hydrolix
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: An MCP server for Hydrolix.
5
5
  Project-URL: Home, https://github.com/hydrolix/mcp-hydrolix
6
6
  License-Expression: Apache-2.0
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.13
9
+ Requires-Dist: certifi>=2026.1.4
9
10
  Requires-Dist: clickhouse-connect<0.11,>=0.10
10
11
  Requires-Dist: fastmcp<2.15,>=2.14
11
12
  Requires-Dist: gunicorn<24.0,>=23.0
@@ -38,3 +38,7 @@ loggers:
38
38
  handlers: [ default ]
39
39
  level: INFO
40
40
  propagate: false
41
+ fastmcp:
42
+ handlers: [ default ]
43
+ level: INFO
44
+ propagate: false
@@ -50,6 +50,7 @@ def main():
50
50
  host=config.mcp_bind_host,
51
51
  port=config.mcp_bind_port,
52
52
  uvicorn_config={"log_config": log_dict_config},
53
+ stateless_http=True,
53
54
  )
54
55
  else:
55
56
  log_dict_config = setup_logging(None, "INFO", "json")
@@ -13,6 +13,7 @@ from dotenv import load_dotenv
13
13
  from fastmcp import FastMCP
14
14
  from fastmcp.exceptions import ToolError
15
15
  from fastmcp.server.dependencies import get_access_token
16
+ from jwt import DecodeError
16
17
  from pydantic import Field
17
18
  from pydantic.dataclasses import dataclass
18
19
  from starlette.requests import Request
@@ -76,14 +77,17 @@ HYDROLIX_CONFIG: Final[HydrolixConfig] = get_config()
76
77
 
77
78
  mcp = FastMCP(
78
79
  name=MCP_SERVER_NAME,
79
- auth=HydrolixCredentialChain(f"https://{HYDROLIX_CONFIG.host}/config"),
80
+ auth=HydrolixCredentialChain(None),
80
81
  )
81
82
 
82
83
 
83
84
  def get_request_credential() -> Optional[HydrolixCredential]:
84
85
  if (token := get_access_token()) is not None:
85
86
  if isinstance(token, AccessToken):
86
- return token.as_credential()
87
+ try:
88
+ return token.as_credential()
89
+ except DecodeError:
90
+ raise ValueError("The provided access token is invalid.")
87
91
  else:
88
92
  raise ValueError(
89
93
  "Found non-hydrolix access token on request -- this should be impossible!"
@@ -127,7 +131,23 @@ async def create_hydrolix_client(pool_mgr, request_credential: Optional[Hydrolix
127
131
  # allow custom hydrolix settings in CH client
128
132
  common.set_setting("invalid_setting_action", "send")
129
133
  common.set_setting("autogenerate_session_id", False)
130
- client_shared_pool = httputil.get_pool_manager(maxsize=HYDROLIX_CONFIG.query_pool_size, num_pools=1)
134
+
135
+ pool_kwargs = {
136
+ "maxsize": HYDROLIX_CONFIG.query_pool_size,
137
+ "num_pools": 1,
138
+ "verify": HYDROLIX_CONFIG.verify,
139
+ }
140
+
141
+ # When verify=True, use certifi CA bundle for SSL verification
142
+ # This ensures we trust modern CAs like Let's Encrypt
143
+ if HYDROLIX_CONFIG.verify:
144
+ pool_kwargs["ca_cert"] = "certifi"
145
+ else:
146
+ import urllib3
147
+
148
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
149
+
150
+ client_shared_pool = httputil.get_pool_manager(**pool_kwargs)
131
151
 
132
152
 
133
153
  def term(*args, **kwargs):
@@ -1,7 +1,7 @@
1
1
  import inspect
2
2
  import ipaddress
3
3
  import json
4
- from datetime import datetime, time
4
+ from datetime import datetime, time, date
5
5
  from decimal import Decimal
6
6
  from functools import wraps
7
7
 
@@ -16,9 +16,9 @@ class ExtendedEncoder(json.JSONEncoder):
16
16
  if isinstance(obj, ipaddress.IPv4Address):
17
17
  return str(obj)
18
18
  if isinstance(obj, datetime):
19
- return obj.time()
20
- if isinstance(obj, time):
21
- return obj.hour * 3600 + obj.minute * 60 + obj.second + obj.microsecond / 1_000_000
19
+ return obj.timestamp()
20
+ if isinstance(obj, (date, time)):
21
+ return obj.isoformat()
22
22
  if isinstance(obj, bytes):
23
23
  return obj.decode()
24
24
  if isinstance(obj, Decimal):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-hydrolix"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "An MCP server for Hydrolix."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -11,6 +11,7 @@ dependencies = [
11
11
  "python-dotenv>=1.1,<1.2",
12
12
  "clickhouse-connect>=0.10,<0.11",
13
13
  "pip-system-certs>=4.0,<5.0",
14
+ "certifi>=2026.1.4",
14
15
  "gunicorn>=23.0,<24.0",
15
16
  "pyjwt>=2.10,<2.11",
16
17
  ]
@@ -23,34 +23,27 @@ class TestExtendedEncoder:
23
23
  """Test that datetime objects are converted to time objects."""
24
24
  dt = datetime(2024, 1, 15, 14, 30, 45, 123456)
25
25
  result = json.dumps({"timestamp": dt}, cls=ExtendedEncoder)
26
- expected_time = dt.time()
27
- expected_seconds = (
28
- expected_time.hour * 3600
29
- + expected_time.minute * 60
30
- + expected_time.second
31
- + expected_time.microsecond / 1_000_000
32
- )
33
- assert result == f'{{"timestamp": {expected_seconds}}}'
26
+ expected_time = dt.timestamp()
27
+ assert result == f'{{"timestamp": {expected_time}}}'
34
28
 
35
29
  def test_time_serialization(self):
36
30
  """Test that time objects are converted to seconds."""
37
31
  t = time(14, 30, 45, 123456)
38
32
  result = json.dumps({"time": t}, cls=ExtendedEncoder)
39
- expected_seconds = 14 * 3600 + 30 * 60 + 45 + 123456 / 1_000_000
40
- assert result == f'{{"time": {expected_seconds}}}'
33
+ expected_time = "14:30:45.123456"
34
+ assert result == f'{{"time": "{expected_time}"}}'
41
35
 
42
36
  def test_time_serialization_midnight(self):
43
37
  """Test time serialization at midnight (edge case)."""
44
38
  t = time(0, 0, 0, 0)
45
39
  result = json.dumps({"time": t}, cls=ExtendedEncoder)
46
- assert result == '{"time": 0.0}'
40
+ assert result == '{"time": "00:00:00"}'
47
41
 
48
42
  def test_time_serialization_end_of_day(self):
49
43
  """Test time serialization at end of day (edge case)."""
50
44
  t = time(23, 59, 59, 999999)
51
45
  result = json.dumps({"time": t}, cls=ExtendedEncoder)
52
- expected_seconds = 23 * 3600 + 59 * 60 + 59 + 999999 / 1_000_000
53
- assert result == f'{{"time": {expected_seconds}}}'
46
+ assert result == '{"time": "23:59:59.999999"}'
54
47
 
55
48
  def test_bytes_serialization(self):
56
49
  """Test that bytes are decoded to strings."""
@@ -88,7 +81,7 @@ class TestExtendedEncoder:
88
81
  parsed = json.loads(result)
89
82
 
90
83
  assert parsed["ip"] == "10.0.0.1"
91
- assert parsed["time"] == 12 * 3600
84
+ assert parsed["time"] == "12:00:00"
92
85
  assert parsed["data"] == "test"
93
86
  assert parsed["amount"] == "99.99"
94
87
 
@@ -222,7 +215,7 @@ class TestWithSerializerDecorator:
222
215
 
223
216
  assert isinstance(result, ToolResult)
224
217
  parsed = result.structured_content
225
- assert parsed["time"] == 10 * 3600 + 30 * 60
218
+ assert parsed["time"] == "10:30:00"
226
219
  assert parsed["decimal"] == "123.45"
227
220
 
228
221
  def test_content_structured_content_match(self):
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.13"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.14'",
@@ -77,11 +77,11 @@ wheels = [
77
77
 
78
78
  [[package]]
79
79
  name = "certifi"
80
- version = "2025.11.12"
80
+ version = "2026.1.4"
81
81
  source = { registry = "https://pypi.org/simple" }
82
- sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
82
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
83
83
  wheels = [
84
- { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
84
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
85
85
  ]
86
86
 
87
87
  [[package]]
@@ -800,9 +800,10 @@ wheels = [
800
800
 
801
801
  [[package]]
802
802
  name = "mcp-hydrolix"
803
- version = "0.1.6"
803
+ version = "0.1.7"
804
804
  source = { editable = "." }
805
805
  dependencies = [
806
+ { name = "certifi" },
806
807
  { name = "clickhouse-connect" },
807
808
  { name = "fastmcp" },
808
809
  { name = "gunicorn" },
@@ -825,6 +826,7 @@ dev = [
825
826
 
826
827
  [package.metadata]
827
828
  requires-dist = [
829
+ { name = "certifi", specifier = ">=2026.1.4" },
828
830
  { name = "clickhouse-connect", specifier = ">=0.10,<0.11" },
829
831
  { name = "fastapi", marker = "extra == 'dev'", specifier = ">=0.124" },
830
832
  { name = "fastmcp", specifier = ">=2.14,<2.15" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes