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.
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/PKG-INFO +2 -1
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/log.yaml +4 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/main.py +1 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/mcp_server.py +23 -3
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/utils.py +4 -4
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/pyproject.toml +2 -1
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_utils.py +8 -15
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/uv.lock +7 -5
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.dockerignore +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.editorconfig +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/pull_request_template.md +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/workflows/publish.yml +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.github/workflows/tests.yaml +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.gitignore +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.pre-commit-config.yaml +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/.python-version +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/Dockerfile +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/LICENSE +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/README.md +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/docker-compose.yaml +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/fastmcp.json +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/glama.json +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/__init__.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/__init__.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/credentials.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/auth/mcp_providers.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/__init__.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/log.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/log/utils.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/mcp_hydrolix/mcp_env.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/test-services/docker-compose.yaml +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/__init__.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_log.py +0 -0
- {mcp_hydrolix-0.1.6 → mcp_hydrolix-0.1.7}/tests/test_mcp_server.py +0 -0
- {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.
|
|
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
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
20
|
-
if isinstance(obj, time):
|
|
21
|
-
return obj.
|
|
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.
|
|
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.
|
|
27
|
-
|
|
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
|
-
|
|
40
|
-
assert result == f'{{"time": {
|
|
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":
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 = "
|
|
80
|
+
version = "2026.1.4"
|
|
81
81
|
source = { registry = "https://pypi.org/simple" }
|
|
82
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
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/
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|