helm-mcp 0.1.3__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.
- helm_mcp-0.1.3/.gitignore +38 -0
- helm_mcp-0.1.3/PKG-INFO +116 -0
- helm_mcp-0.1.3/README.md +87 -0
- helm_mcp-0.1.3/pyproject.toml +69 -0
- helm_mcp-0.1.3/src/helm_mcp/__init__.py +27 -0
- helm_mcp-0.1.3/src/helm_mcp/cli.py +51 -0
- helm_mcp-0.1.3/src/helm_mcp/client.py +40 -0
- helm_mcp-0.1.3/src/helm_mcp/py.typed +0 -0
- helm_mcp-0.1.3/src/helm_mcp/server.py +187 -0
- helm_mcp-0.1.3/tests/__init__.py +0 -0
- helm_mcp-0.1.3/tests/test_server.py +505 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Go build output (root-level binary only)
|
|
2
|
+
/helm-mcp
|
|
3
|
+
/helm-mcp-*
|
|
4
|
+
coverage.out
|
|
5
|
+
coverage.html
|
|
6
|
+
*.exe
|
|
7
|
+
vendor/
|
|
8
|
+
|
|
9
|
+
# Coverage reports
|
|
10
|
+
python_coverage*.xml
|
|
11
|
+
python/.coverage
|
|
12
|
+
|
|
13
|
+
# Python
|
|
14
|
+
python/.venv/
|
|
15
|
+
python/dist/
|
|
16
|
+
python/build/
|
|
17
|
+
python/*.egg-info/
|
|
18
|
+
python/src/*.egg-info/
|
|
19
|
+
__pycache__/
|
|
20
|
+
*.pyc
|
|
21
|
+
*.pyo
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
|
|
25
|
+
# IDE / Editor
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
*~
|
|
31
|
+
.DS_Store
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
Thumbs.db
|
|
35
|
+
|
|
36
|
+
# Environment
|
|
37
|
+
.env
|
|
38
|
+
.env.local
|
helm_mcp-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: helm-mcp
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: MCP server exposing all Helm CLI capabilities via the Model Context Protocol
|
|
5
|
+
Project-URL: Homepage, https://github.com/SCGIS-Wales/helm-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/SCGIS-Wales/helm-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/SCGIS-Wales/helm-mcp/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/SCGIS-Wales/helm-mcp#python-package
|
|
9
|
+
Project-URL: Changelog, https://github.com/SCGIS-Wales/helm-mcp/releases
|
|
10
|
+
Author: sreengineer
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
Keywords: ai-tools,fastmcp,helm,kubernetes,mcp,model-context-protocol
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Requires-Dist: fastmcp>=3.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# helm-mcp (Python)
|
|
31
|
+
|
|
32
|
+
A Python MCP wrapper for the [helm-mcp](https://github.com/ssddgreg/helm-mcp) Go server.
|
|
33
|
+
|
|
34
|
+
Uses [FastMCP](https://github.com/PrefectHQ/fastmcp) to create a transparent proxy around the helm-mcp Go binary, exposing all Helm tools via the Model Context Protocol. **New tools added to the Go binary are automatically available without any Python code changes.**
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.14+
|
|
39
|
+
- The `helm-mcp` Go binary on your `PATH` or pointed to via `HELM_MCP_BINARY`
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install helm-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### As a server
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from helm_mcp import create_server
|
|
53
|
+
|
|
54
|
+
server = create_server()
|
|
55
|
+
server.run() # stdio mode (default)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### As a client
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from helm_mcp import create_client
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with create_client() as client:
|
|
66
|
+
tools = await client.list_tools()
|
|
67
|
+
print(f"Available tools: {len(tools)}")
|
|
68
|
+
|
|
69
|
+
result = await client.call_tool("helm_list", {"namespace": "default"})
|
|
70
|
+
print(result)
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# stdio mode (default, for MCP clients like Claude Code)
|
|
79
|
+
helm-mcp-python
|
|
80
|
+
|
|
81
|
+
# HTTP mode
|
|
82
|
+
helm-mcp-python --transport http --host 0.0.0.0 --port 8080
|
|
83
|
+
|
|
84
|
+
# Explicit binary path
|
|
85
|
+
helm-mcp-python --binary /usr/local/bin/helm-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Binary Discovery
|
|
89
|
+
|
|
90
|
+
The package locates the `helm-mcp` Go binary in this order:
|
|
91
|
+
|
|
92
|
+
1. `HELM_MCP_BINARY` environment variable
|
|
93
|
+
2. Bundled binary in the package `bin/` directory
|
|
94
|
+
3. `helm-mcp` on `PATH`
|
|
95
|
+
|
|
96
|
+
## Environment Variables
|
|
97
|
+
|
|
98
|
+
The proxy forwards these environment variables to the Go binary:
|
|
99
|
+
|
|
100
|
+
| Category | Variables |
|
|
101
|
+
|----------|-----------|
|
|
102
|
+
| Proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and lowercase variants) |
|
|
103
|
+
| Kubernetes | `KUBECONFIG`, `KUBERNETES_SERVICE_HOST`, `KUBERNETES_SERVICE_PORT` |
|
|
104
|
+
| Helm | `HELM_CACHE_HOME`, `HELM_CONFIG_HOME`, `HELM_DATA_HOME`, `HELM_PLUGINS`, `HELM_DEBUG` |
|
|
105
|
+
| AWS | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, `AWS_PROFILE` |
|
|
106
|
+
| GCP | `GOOGLE_APPLICATION_CREDENTIALS`, `CLOUDSDK_COMPUTE_ZONE` |
|
|
107
|
+
| Azure | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID` |
|
|
108
|
+
| TLS | `SSL_CERT_FILE`, `SSL_CERT_DIR` |
|
|
109
|
+
|
|
110
|
+
## Scalability
|
|
111
|
+
|
|
112
|
+
This package uses the MCP proxy pattern: the Python layer never needs to know about individual Helm tools. All tool discovery, input schemas, and invocations are forwarded to the Go binary via the MCP protocol at runtime. When new capabilities are added to the Go server, they are immediately available through the Python wrapper.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
helm_mcp-0.1.3/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# helm-mcp (Python)
|
|
2
|
+
|
|
3
|
+
A Python MCP wrapper for the [helm-mcp](https://github.com/ssddgreg/helm-mcp) Go server.
|
|
4
|
+
|
|
5
|
+
Uses [FastMCP](https://github.com/PrefectHQ/fastmcp) to create a transparent proxy around the helm-mcp Go binary, exposing all Helm tools via the Model Context Protocol. **New tools added to the Go binary are automatically available without any Python code changes.**
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.14+
|
|
10
|
+
- The `helm-mcp` Go binary on your `PATH` or pointed to via `HELM_MCP_BINARY`
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install helm-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### As a server
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from helm_mcp import create_server
|
|
24
|
+
|
|
25
|
+
server = create_server()
|
|
26
|
+
server.run() # stdio mode (default)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### As a client
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from helm_mcp import create_client
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
async with create_client() as client:
|
|
37
|
+
tools = await client.list_tools()
|
|
38
|
+
print(f"Available tools: {len(tools)}")
|
|
39
|
+
|
|
40
|
+
result = await client.call_tool("helm_list", {"namespace": "default"})
|
|
41
|
+
print(result)
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CLI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# stdio mode (default, for MCP clients like Claude Code)
|
|
50
|
+
helm-mcp-python
|
|
51
|
+
|
|
52
|
+
# HTTP mode
|
|
53
|
+
helm-mcp-python --transport http --host 0.0.0.0 --port 8080
|
|
54
|
+
|
|
55
|
+
# Explicit binary path
|
|
56
|
+
helm-mcp-python --binary /usr/local/bin/helm-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Binary Discovery
|
|
60
|
+
|
|
61
|
+
The package locates the `helm-mcp` Go binary in this order:
|
|
62
|
+
|
|
63
|
+
1. `HELM_MCP_BINARY` environment variable
|
|
64
|
+
2. Bundled binary in the package `bin/` directory
|
|
65
|
+
3. `helm-mcp` on `PATH`
|
|
66
|
+
|
|
67
|
+
## Environment Variables
|
|
68
|
+
|
|
69
|
+
The proxy forwards these environment variables to the Go binary:
|
|
70
|
+
|
|
71
|
+
| Category | Variables |
|
|
72
|
+
|----------|-----------|
|
|
73
|
+
| Proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and lowercase variants) |
|
|
74
|
+
| Kubernetes | `KUBECONFIG`, `KUBERNETES_SERVICE_HOST`, `KUBERNETES_SERVICE_PORT` |
|
|
75
|
+
| Helm | `HELM_CACHE_HOME`, `HELM_CONFIG_HOME`, `HELM_DATA_HOME`, `HELM_PLUGINS`, `HELM_DEBUG` |
|
|
76
|
+
| AWS | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, `AWS_PROFILE` |
|
|
77
|
+
| GCP | `GOOGLE_APPLICATION_CREDENTIALS`, `CLOUDSDK_COMPUTE_ZONE` |
|
|
78
|
+
| Azure | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID` |
|
|
79
|
+
| TLS | `SSL_CERT_FILE`, `SSL_CERT_DIR` |
|
|
80
|
+
|
|
81
|
+
## Scalability
|
|
82
|
+
|
|
83
|
+
This package uses the MCP proxy pattern: the Python layer never needs to know about individual Helm tools. All tool discovery, input schemas, and invocations are forwarded to the Go binary via the MCP protocol at runtime. When new capabilities are added to the Go server, they are immediately available through the Python wrapper.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "helm-mcp"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "MCP server exposing all Helm CLI capabilities via the Model Context Protocol"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "sreengineer" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["helm", "kubernetes", "mcp", "model-context-protocol", "fastmcp", "ai-tools"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Topic :: System :: Systems Administration",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"fastmcp>=3.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-asyncio>=0.24",
|
|
35
|
+
"ruff>=0.9",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/SCGIS-Wales/helm-mcp"
|
|
40
|
+
Repository = "https://github.com/SCGIS-Wales/helm-mcp"
|
|
41
|
+
Issues = "https://github.com/SCGIS-Wales/helm-mcp/issues"
|
|
42
|
+
Documentation = "https://github.com/SCGIS-Wales/helm-mcp#python-package"
|
|
43
|
+
Changelog = "https://github.com/SCGIS-Wales/helm-mcp/releases"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
helm-mcp-python = "helm_mcp.cli:main"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/helm_mcp"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = [
|
|
53
|
+
"src/helm_mcp/",
|
|
54
|
+
"tests/",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE",
|
|
57
|
+
"pyproject.toml",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
target-version = "py314"
|
|
66
|
+
line-length = 100
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""helm-mcp: A Python MCP wrapper for the Helm MCP server.
|
|
2
|
+
|
|
3
|
+
This package provides a FastMCP proxy that wraps the helm-mcp Go binary,
|
|
4
|
+
automatically exposing all Helm tools via the Model Context Protocol.
|
|
5
|
+
|
|
6
|
+
The proxy pattern means this package requires zero code changes when new
|
|
7
|
+
tools are added to the Go binary — they are discovered and forwarded
|
|
8
|
+
automatically at runtime via the MCP protocol.
|
|
9
|
+
|
|
10
|
+
Usage as a server:
|
|
11
|
+
from helm_mcp import create_server
|
|
12
|
+
server = create_server()
|
|
13
|
+
server.run()
|
|
14
|
+
|
|
15
|
+
Usage as a client:
|
|
16
|
+
from helm_mcp import create_client
|
|
17
|
+
async with create_client() as client:
|
|
18
|
+
tools = await client.list_tools()
|
|
19
|
+
result = await client.call_tool("helm_list", {"namespace": "default"})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.3"
|
|
23
|
+
|
|
24
|
+
from helm_mcp.client import create_client
|
|
25
|
+
from helm_mcp.server import create_server
|
|
26
|
+
|
|
27
|
+
__all__ = ["create_server", "create_client", "__version__"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CLI entry point for helm-mcp-python."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""Run the helm-mcp proxy server."""
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="helm-mcp: MCP server for Helm operations",
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--transport",
|
|
14
|
+
choices=["stdio", "http", "sse"],
|
|
15
|
+
default="stdio",
|
|
16
|
+
help="Transport mode (default: stdio)",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--host",
|
|
20
|
+
default="0.0.0.0",
|
|
21
|
+
help="Host for HTTP/SSE mode (default: 0.0.0.0)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--port",
|
|
25
|
+
type=int,
|
|
26
|
+
default=8080,
|
|
27
|
+
help="Port for HTTP/SSE mode (default: 8080)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--binary",
|
|
31
|
+
default=None,
|
|
32
|
+
help="Path to helm-mcp Go binary (auto-detected if not set)",
|
|
33
|
+
)
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
from helm_mcp.server import create_server
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
server = create_server(binary_path=args.binary)
|
|
40
|
+
except FileNotFoundError as e:
|
|
41
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
if args.transport == "stdio":
|
|
45
|
+
server.run()
|
|
46
|
+
else:
|
|
47
|
+
server.run(transport=args.transport, host=args.host, port=args.port)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""FastMCP client for connecting to the helm-mcp Go binary.
|
|
2
|
+
|
|
3
|
+
Provides a thin client wrapper that connects to the Go binary via stdio
|
|
4
|
+
transport. All tool discovery is handled by the MCP protocol at runtime,
|
|
5
|
+
so new tools added to the binary are automatically available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastmcp import Client
|
|
9
|
+
from fastmcp.client.transports import StdioTransport
|
|
10
|
+
|
|
11
|
+
from helm_mcp.server import _build_subprocess_env, _find_binary
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_client(
|
|
15
|
+
binary_path: str | None = None,
|
|
16
|
+
env: dict[str, str] | None = None,
|
|
17
|
+
) -> Client:
|
|
18
|
+
"""Create a FastMCP client connected to the helm-mcp Go binary via stdio.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
binary_path: Explicit path to the helm-mcp binary. Auto-detected if ``None``.
|
|
22
|
+
env: Additional environment variables to pass to the subprocess.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A FastMCP ``Client`` instance. Use as an async context manager.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
async with create_client() as client:
|
|
30
|
+
tools = await client.list_tools()
|
|
31
|
+
result = await client.call_tool("helm_list", {"namespace": "default"})
|
|
32
|
+
"""
|
|
33
|
+
binary = binary_path or _find_binary()
|
|
34
|
+
subprocess_env = _build_subprocess_env(extra_env=env)
|
|
35
|
+
transport = StdioTransport(
|
|
36
|
+
command=binary,
|
|
37
|
+
args=["--mode", "stdio"],
|
|
38
|
+
env=subprocess_env or None,
|
|
39
|
+
)
|
|
40
|
+
return Client(transport)
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""FastMCP proxy server wrapping the helm-mcp Go binary.
|
|
2
|
+
|
|
3
|
+
The proxy pattern ensures forward-compatibility: when new tools are added
|
|
4
|
+
to the Go binary, they are automatically discovered and exposed by the
|
|
5
|
+
proxy without any Python code changes. The MCP protocol handles tool
|
|
6
|
+
discovery at runtime via the ``tools/list`` method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fastmcp.client.transports import StdioTransport
|
|
15
|
+
from fastmcp.server import create_proxy
|
|
16
|
+
|
|
17
|
+
# Environment variables forwarded to the Go subprocess.
|
|
18
|
+
# Extend this list to pass additional variables — the proxy itself
|
|
19
|
+
# never needs to know about individual Helm tools.
|
|
20
|
+
PASSTHROUGH_ENV_VARS: list[str] = [
|
|
21
|
+
# Core system
|
|
22
|
+
"HOME",
|
|
23
|
+
"USER",
|
|
24
|
+
"PATH",
|
|
25
|
+
# Kubernetes
|
|
26
|
+
"KUBECONFIG",
|
|
27
|
+
"KUBERNETES_SERVICE_HOST",
|
|
28
|
+
"KUBERNETES_SERVICE_PORT",
|
|
29
|
+
# Forward proxy
|
|
30
|
+
"HTTP_PROXY",
|
|
31
|
+
"HTTPS_PROXY",
|
|
32
|
+
"NO_PROXY",
|
|
33
|
+
"http_proxy",
|
|
34
|
+
"https_proxy",
|
|
35
|
+
"no_proxy",
|
|
36
|
+
# Helm-specific
|
|
37
|
+
"HELM_CACHE_HOME",
|
|
38
|
+
"HELM_CONFIG_HOME",
|
|
39
|
+
"HELM_DATA_HOME",
|
|
40
|
+
"HELM_DRIVER",
|
|
41
|
+
"HELM_REGISTRY_CONFIG",
|
|
42
|
+
"HELM_REPOSITORY_CACHE",
|
|
43
|
+
"HELM_REPOSITORY_CONFIG",
|
|
44
|
+
"HELM_PLUGINS",
|
|
45
|
+
"HELM_DEBUG",
|
|
46
|
+
# AWS (EKS)
|
|
47
|
+
"AWS_ACCESS_KEY_ID",
|
|
48
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
49
|
+
"AWS_SESSION_TOKEN",
|
|
50
|
+
"AWS_DEFAULT_REGION",
|
|
51
|
+
"AWS_REGION",
|
|
52
|
+
"AWS_PROFILE",
|
|
53
|
+
"AWS_SHARED_CREDENTIALS_FILE",
|
|
54
|
+
"AWS_CONFIG_FILE",
|
|
55
|
+
# Google Cloud (GKE)
|
|
56
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
57
|
+
"CLOUDSDK_COMPUTE_ZONE",
|
|
58
|
+
"CLOUDSDK_COMPUTE_REGION",
|
|
59
|
+
"CLOUDSDK_CORE_PROJECT",
|
|
60
|
+
# Azure (AKS)
|
|
61
|
+
"AZURE_TENANT_ID",
|
|
62
|
+
"AZURE_CLIENT_ID",
|
|
63
|
+
"AZURE_CLIENT_SECRET",
|
|
64
|
+
"AZURE_SUBSCRIPTION_ID",
|
|
65
|
+
"AZURE_AUTHORITY_HOST",
|
|
66
|
+
# TLS / CA
|
|
67
|
+
"SSL_CERT_FILE",
|
|
68
|
+
"SSL_CERT_DIR",
|
|
69
|
+
"REQUESTS_CA_BUNDLE",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _find_binary() -> str:
|
|
74
|
+
"""Locate the helm-mcp binary.
|
|
75
|
+
|
|
76
|
+
Search order:
|
|
77
|
+
1. ``HELM_MCP_BINARY`` environment variable
|
|
78
|
+
2. Bundled binary in the package ``bin/`` directory
|
|
79
|
+
3. ``helm-mcp`` on ``PATH``
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Absolute path to the helm-mcp executable.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
FileNotFoundError: If the binary cannot be located.
|
|
86
|
+
"""
|
|
87
|
+
# 1. Explicit env var
|
|
88
|
+
env_path = os.environ.get("HELM_MCP_BINARY")
|
|
89
|
+
if env_path:
|
|
90
|
+
p = Path(env_path)
|
|
91
|
+
if p.is_file() and os.access(str(p), os.X_OK):
|
|
92
|
+
return str(p)
|
|
93
|
+
raise FileNotFoundError(f"HELM_MCP_BINARY={env_path} does not exist or is not executable")
|
|
94
|
+
|
|
95
|
+
# 2. Bundled binary in package data
|
|
96
|
+
pkg_dir = Path(__file__).parent
|
|
97
|
+
system = platform.system().lower()
|
|
98
|
+
machine = platform.machine().lower()
|
|
99
|
+
arch_map = {"x86_64": "amd64", "aarch64": "arm64", "arm64": "arm64", "amd64": "amd64"}
|
|
100
|
+
arch = arch_map.get(machine, machine)
|
|
101
|
+
binary_name = f"helm-mcp-{system}-{arch}"
|
|
102
|
+
if system == "windows":
|
|
103
|
+
binary_name += ".exe"
|
|
104
|
+
bundled = pkg_dir / "bin" / binary_name
|
|
105
|
+
if bundled.is_file() and os.access(str(bundled), os.X_OK):
|
|
106
|
+
return str(bundled)
|
|
107
|
+
|
|
108
|
+
# Also check for plain "helm-mcp" in bin/
|
|
109
|
+
plain = pkg_dir / "bin" / "helm-mcp"
|
|
110
|
+
if plain.is_file() and os.access(str(plain), os.X_OK):
|
|
111
|
+
return str(plain)
|
|
112
|
+
|
|
113
|
+
# 3. PATH lookup
|
|
114
|
+
found = shutil.which("helm-mcp")
|
|
115
|
+
if found:
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
raise FileNotFoundError(
|
|
119
|
+
"helm-mcp binary not found. Either:\n"
|
|
120
|
+
" 1. Set HELM_MCP_BINARY=/path/to/helm-mcp\n"
|
|
121
|
+
" 2. Install helm-mcp and ensure it's on your PATH\n"
|
|
122
|
+
" 3. Install the platform-specific wheel (pip install helm-mcp[binary])"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_subprocess_env(
|
|
127
|
+
extra_env: dict[str, str] | None = None,
|
|
128
|
+
passthrough: list[str] | None = None,
|
|
129
|
+
) -> dict[str, str]:
|
|
130
|
+
"""Build the environment dict for the Go subprocess.
|
|
131
|
+
|
|
132
|
+
Collects variables from ``PASSTHROUGH_ENV_VARS`` (or a custom list)
|
|
133
|
+
and merges in any extra overrides.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
extra_env: Additional variables that take precedence.
|
|
137
|
+
passthrough: Override the default passthrough list.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Environment dict for subprocess execution.
|
|
141
|
+
"""
|
|
142
|
+
vars_to_pass = passthrough or PASSTHROUGH_ENV_VARS
|
|
143
|
+
env: dict[str, str] = {}
|
|
144
|
+
for var in vars_to_pass:
|
|
145
|
+
val = os.environ.get(var)
|
|
146
|
+
if val is not None:
|
|
147
|
+
env[var] = val
|
|
148
|
+
if extra_env:
|
|
149
|
+
env.update(extra_env)
|
|
150
|
+
return env
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_server(
|
|
154
|
+
binary_path: str | None = None,
|
|
155
|
+
name: str = "helm-mcp",
|
|
156
|
+
env: dict[str, str] | None = None,
|
|
157
|
+
):
|
|
158
|
+
"""Create a FastMCP proxy server wrapping the helm-mcp Go binary.
|
|
159
|
+
|
|
160
|
+
The proxy transparently forwards all MCP requests to the Go binary,
|
|
161
|
+
which means any new tools added to the binary are automatically
|
|
162
|
+
available without changing this Python code.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
binary_path: Explicit path to the helm-mcp binary. Auto-detected if ``None``.
|
|
166
|
+
name: Server name advertised via MCP.
|
|
167
|
+
env: Additional environment variables to pass to the subprocess.
|
|
168
|
+
These are merged on top of the default passthrough list
|
|
169
|
+
(``PASSTHROUGH_ENV_VARS``).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A FastMCP server instance ready to run.
|
|
173
|
+
|
|
174
|
+
Example::
|
|
175
|
+
|
|
176
|
+
server = create_server()
|
|
177
|
+
server.run() # stdio
|
|
178
|
+
server.run(transport="http", host="0.0.0.0", port=8080) # HTTP
|
|
179
|
+
"""
|
|
180
|
+
binary = binary_path or _find_binary()
|
|
181
|
+
subprocess_env = _build_subprocess_env(extra_env=env)
|
|
182
|
+
transport = StdioTransport(
|
|
183
|
+
command=binary,
|
|
184
|
+
args=["--mode", "stdio"],
|
|
185
|
+
env=subprocess_env or None,
|
|
186
|
+
)
|
|
187
|
+
return create_proxy(transport, name=name)
|
|
File without changes
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""Tests for helm_mcp server and client modules."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# Package metadata
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_version():
|
|
16
|
+
"""Test package version is set."""
|
|
17
|
+
from helm_mcp import __version__
|
|
18
|
+
|
|
19
|
+
assert __version__ # version is set (value managed by auto-tag)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_exports():
|
|
23
|
+
"""Test that public API is properly exported."""
|
|
24
|
+
import helm_mcp
|
|
25
|
+
|
|
26
|
+
assert hasattr(helm_mcp, "create_server")
|
|
27
|
+
assert hasattr(helm_mcp, "create_client")
|
|
28
|
+
assert hasattr(helm_mcp, "__version__")
|
|
29
|
+
assert callable(helm_mcp.create_server)
|
|
30
|
+
assert callable(helm_mcp.create_client)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_all_exports():
|
|
34
|
+
"""Test __all__ matches expected exports."""
|
|
35
|
+
import helm_mcp
|
|
36
|
+
|
|
37
|
+
assert set(helm_mcp.__all__) == {"create_server", "create_client", "__version__"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_py_typed_marker():
|
|
41
|
+
"""Test PEP 561 py.typed marker exists."""
|
|
42
|
+
import helm_mcp
|
|
43
|
+
|
|
44
|
+
pkg_dir = Path(helm_mcp.__file__).parent
|
|
45
|
+
assert (pkg_dir / "py.typed").exists()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Binary discovery
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_find_binary_env_var(tmp_path):
|
|
54
|
+
"""Test binary discovery via HELM_MCP_BINARY env var."""
|
|
55
|
+
from helm_mcp.server import _find_binary
|
|
56
|
+
|
|
57
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
58
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
59
|
+
fake_binary.chmod(0o755)
|
|
60
|
+
|
|
61
|
+
with patch.dict(os.environ, {"HELM_MCP_BINARY": str(fake_binary)}):
|
|
62
|
+
result = _find_binary()
|
|
63
|
+
assert result == str(fake_binary)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_find_binary_env_var_not_found():
|
|
67
|
+
"""Test error when HELM_MCP_BINARY points to nonexistent file."""
|
|
68
|
+
from helm_mcp.server import _find_binary
|
|
69
|
+
|
|
70
|
+
with (
|
|
71
|
+
patch.dict(os.environ, {"HELM_MCP_BINARY": "/nonexistent/helm-mcp"}),
|
|
72
|
+
pytest.raises(FileNotFoundError, match="HELM_MCP_BINARY"),
|
|
73
|
+
):
|
|
74
|
+
_find_binary()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_find_binary_env_var_not_executable(tmp_path):
|
|
78
|
+
"""Test error when HELM_MCP_BINARY exists but is not executable."""
|
|
79
|
+
from helm_mcp.server import _find_binary
|
|
80
|
+
|
|
81
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
82
|
+
fake_binary.write_text("not executable")
|
|
83
|
+
fake_binary.chmod(0o644)
|
|
84
|
+
|
|
85
|
+
with (
|
|
86
|
+
patch.dict(os.environ, {"HELM_MCP_BINARY": str(fake_binary)}),
|
|
87
|
+
pytest.raises(FileNotFoundError, match="HELM_MCP_BINARY"),
|
|
88
|
+
):
|
|
89
|
+
_find_binary()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_find_binary_path_lookup(tmp_path):
|
|
93
|
+
"""Test binary discovery via PATH."""
|
|
94
|
+
from helm_mcp.server import _find_binary
|
|
95
|
+
|
|
96
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
97
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
98
|
+
fake_binary.chmod(0o755)
|
|
99
|
+
|
|
100
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
101
|
+
env["PATH"] = f"{tmp_path}:{env.get('PATH', '')}"
|
|
102
|
+
|
|
103
|
+
with patch.dict(os.environ, env, clear=True):
|
|
104
|
+
result = _find_binary()
|
|
105
|
+
assert result == str(fake_binary)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_find_binary_not_found():
|
|
109
|
+
"""Test error when binary cannot be found."""
|
|
110
|
+
from helm_mcp.server import _find_binary
|
|
111
|
+
|
|
112
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
113
|
+
env["PATH"] = "/nonexistent"
|
|
114
|
+
|
|
115
|
+
with (
|
|
116
|
+
patch.dict(os.environ, env, clear=True),
|
|
117
|
+
pytest.raises(FileNotFoundError, match="helm-mcp binary not found"),
|
|
118
|
+
):
|
|
119
|
+
_find_binary()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_find_binary_bundled(tmp_path):
|
|
123
|
+
"""Test binary discovery from bundled bin/ directory."""
|
|
124
|
+
import helm_mcp.server as server_mod
|
|
125
|
+
from helm_mcp.server import _find_binary
|
|
126
|
+
|
|
127
|
+
pkg_dir = Path(server_mod.__file__).parent
|
|
128
|
+
bin_dir = pkg_dir / "bin"
|
|
129
|
+
bin_dir.mkdir(exist_ok=True)
|
|
130
|
+
bundled = bin_dir / "helm-mcp"
|
|
131
|
+
bundled.write_text("#!/bin/sh\necho hello")
|
|
132
|
+
bundled.chmod(0o755)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
136
|
+
env["PATH"] = "/nonexistent"
|
|
137
|
+
with patch.dict(os.environ, env, clear=True):
|
|
138
|
+
result = _find_binary()
|
|
139
|
+
assert result == str(bundled)
|
|
140
|
+
finally:
|
|
141
|
+
bundled.unlink()
|
|
142
|
+
bin_dir.rmdir()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_find_binary_bundled_platform_specific(tmp_path):
|
|
146
|
+
"""Test platform-specific bundled binary discovery."""
|
|
147
|
+
import helm_mcp.server as server_mod
|
|
148
|
+
from helm_mcp.server import _find_binary
|
|
149
|
+
|
|
150
|
+
pkg_dir = Path(server_mod.__file__).parent
|
|
151
|
+
bin_dir = pkg_dir / "bin"
|
|
152
|
+
bin_dir.mkdir(exist_ok=True)
|
|
153
|
+
|
|
154
|
+
system = platform.system().lower()
|
|
155
|
+
machine = platform.machine().lower()
|
|
156
|
+
arch_map = {"x86_64": "amd64", "aarch64": "arm64", "arm64": "arm64", "amd64": "amd64"}
|
|
157
|
+
arch = arch_map.get(machine, machine)
|
|
158
|
+
binary_name = f"helm-mcp-{system}-{arch}"
|
|
159
|
+
|
|
160
|
+
bundled = bin_dir / binary_name
|
|
161
|
+
bundled.write_text("#!/bin/sh\necho hello")
|
|
162
|
+
bundled.chmod(0o755)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
166
|
+
env["PATH"] = "/nonexistent"
|
|
167
|
+
with patch.dict(os.environ, env, clear=True):
|
|
168
|
+
result = _find_binary()
|
|
169
|
+
assert result == str(bundled)
|
|
170
|
+
finally:
|
|
171
|
+
bundled.unlink()
|
|
172
|
+
bin_dir.rmdir()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Environment building
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_build_subprocess_env_passthrough():
|
|
181
|
+
"""Test that _build_subprocess_env forwards expected variables."""
|
|
182
|
+
from helm_mcp.server import _build_subprocess_env
|
|
183
|
+
|
|
184
|
+
test_vars = {
|
|
185
|
+
"HTTP_PROXY": "http://proxy:8080",
|
|
186
|
+
"HTTPS_PROXY": "http://proxy:8443",
|
|
187
|
+
"NO_PROXY": "localhost,.internal",
|
|
188
|
+
"KUBECONFIG": "/home/user/.kube/config",
|
|
189
|
+
"HOME": "/home/user",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
with patch.dict(os.environ, test_vars, clear=True):
|
|
193
|
+
result = _build_subprocess_env()
|
|
194
|
+
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
|
195
|
+
assert result["HTTPS_PROXY"] == "http://proxy:8443"
|
|
196
|
+
assert result["NO_PROXY"] == "localhost,.internal"
|
|
197
|
+
assert result["KUBECONFIG"] == "/home/user/.kube/config"
|
|
198
|
+
assert result["HOME"] == "/home/user"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_build_subprocess_env_extra_overrides():
|
|
202
|
+
"""Test that extra_env overrides passthrough values."""
|
|
203
|
+
from helm_mcp.server import _build_subprocess_env
|
|
204
|
+
|
|
205
|
+
with patch.dict(os.environ, {"HOME": "/home/user"}, clear=True):
|
|
206
|
+
result = _build_subprocess_env(extra_env={"HOME": "/override", "CUSTOM": "value"})
|
|
207
|
+
assert result["HOME"] == "/override"
|
|
208
|
+
assert result["CUSTOM"] == "value"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_build_subprocess_env_custom_passthrough():
|
|
212
|
+
"""Test _build_subprocess_env with a custom passthrough list."""
|
|
213
|
+
from helm_mcp.server import _build_subprocess_env
|
|
214
|
+
|
|
215
|
+
with patch.dict(os.environ, {"FOO": "bar", "HOME": "/home/user"}, clear=True):
|
|
216
|
+
result = _build_subprocess_env(passthrough=["FOO"])
|
|
217
|
+
assert result == {"FOO": "bar"}
|
|
218
|
+
assert "HOME" not in result
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_build_subprocess_env_skips_unset():
|
|
222
|
+
"""Test that unset variables are not included."""
|
|
223
|
+
from helm_mcp.server import _build_subprocess_env
|
|
224
|
+
|
|
225
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
226
|
+
result = _build_subprocess_env()
|
|
227
|
+
assert result == {}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_build_subprocess_env_empty_extra():
|
|
231
|
+
"""Test passing empty extra_env dict."""
|
|
232
|
+
from helm_mcp.server import _build_subprocess_env
|
|
233
|
+
|
|
234
|
+
with patch.dict(os.environ, {"HOME": "/home/user"}, clear=True):
|
|
235
|
+
result = _build_subprocess_env(extra_env={})
|
|
236
|
+
assert result["HOME"] == "/home/user"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# PASSTHROUGH_ENV_VARS completeness
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_passthrough_env_vars_includes_proxy():
|
|
245
|
+
"""Test that PASSTHROUGH_ENV_VARS includes all proxy variants."""
|
|
246
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
247
|
+
|
|
248
|
+
for var in ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]:
|
|
249
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_passthrough_env_vars_includes_kubernetes():
|
|
253
|
+
"""Test that PASSTHROUGH_ENV_VARS includes Kubernetes variables."""
|
|
254
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
255
|
+
|
|
256
|
+
for var in ["KUBECONFIG", "KUBERNETES_SERVICE_HOST", "KUBERNETES_SERVICE_PORT"]:
|
|
257
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_passthrough_env_vars_includes_helm():
|
|
261
|
+
"""Test that PASSTHROUGH_ENV_VARS includes Helm variables."""
|
|
262
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
263
|
+
|
|
264
|
+
for var in [
|
|
265
|
+
"HELM_CACHE_HOME",
|
|
266
|
+
"HELM_CONFIG_HOME",
|
|
267
|
+
"HELM_DATA_HOME",
|
|
268
|
+
"HELM_DRIVER",
|
|
269
|
+
"HELM_PLUGINS",
|
|
270
|
+
"HELM_DEBUG",
|
|
271
|
+
]:
|
|
272
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_passthrough_env_vars_includes_aws():
|
|
276
|
+
"""Test that PASSTHROUGH_ENV_VARS includes AWS variables."""
|
|
277
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
278
|
+
|
|
279
|
+
for var in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_PROFILE"]:
|
|
280
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_passthrough_env_vars_includes_gcp():
|
|
284
|
+
"""Test that PASSTHROUGH_ENV_VARS includes GCP variables."""
|
|
285
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
286
|
+
|
|
287
|
+
assert "GOOGLE_APPLICATION_CREDENTIALS" in PASSTHROUGH_ENV_VARS
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_passthrough_env_vars_includes_azure():
|
|
291
|
+
"""Test that PASSTHROUGH_ENV_VARS includes Azure variables."""
|
|
292
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
293
|
+
|
|
294
|
+
for var in ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"]:
|
|
295
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_passthrough_env_vars_includes_tls():
|
|
299
|
+
"""Test that PASSTHROUGH_ENV_VARS includes TLS CA variables."""
|
|
300
|
+
from helm_mcp.server import PASSTHROUGH_ENV_VARS
|
|
301
|
+
|
|
302
|
+
for var in ["SSL_CERT_FILE", "SSL_CERT_DIR"]:
|
|
303
|
+
assert var in PASSTHROUGH_ENV_VARS, f"{var} missing from PASSTHROUGH_ENV_VARS"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Server / Client creation
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_create_server_with_binary(tmp_path):
|
|
312
|
+
"""Test create_server with explicit binary path."""
|
|
313
|
+
from helm_mcp.server import create_server
|
|
314
|
+
|
|
315
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
316
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
317
|
+
fake_binary.chmod(0o755)
|
|
318
|
+
|
|
319
|
+
server = create_server(binary_path=str(fake_binary))
|
|
320
|
+
assert server is not None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_create_server_custom_name(tmp_path):
|
|
324
|
+
"""Test create_server with a custom server name."""
|
|
325
|
+
from helm_mcp.server import create_server
|
|
326
|
+
|
|
327
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
328
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
329
|
+
fake_binary.chmod(0o755)
|
|
330
|
+
|
|
331
|
+
server = create_server(binary_path=str(fake_binary), name="my-helm-mcp")
|
|
332
|
+
assert server is not None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_create_server_with_extra_env(tmp_path):
|
|
336
|
+
"""Test create_server with extra environment variables."""
|
|
337
|
+
from helm_mcp.server import create_server
|
|
338
|
+
|
|
339
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
340
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
341
|
+
fake_binary.chmod(0o755)
|
|
342
|
+
|
|
343
|
+
server = create_server(
|
|
344
|
+
binary_path=str(fake_binary),
|
|
345
|
+
env={"CUSTOM_VAR": "custom_value"},
|
|
346
|
+
)
|
|
347
|
+
assert server is not None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_create_server_binary_not_found():
|
|
351
|
+
"""Test create_server raises when binary not found."""
|
|
352
|
+
from helm_mcp.server import create_server
|
|
353
|
+
|
|
354
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
355
|
+
env["PATH"] = "/nonexistent"
|
|
356
|
+
|
|
357
|
+
with patch.dict(os.environ, env, clear=True), pytest.raises(FileNotFoundError):
|
|
358
|
+
create_server()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_create_client_with_binary(tmp_path):
|
|
362
|
+
"""Test create_client with explicit binary path."""
|
|
363
|
+
from helm_mcp.client import create_client
|
|
364
|
+
|
|
365
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
366
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
367
|
+
fake_binary.chmod(0o755)
|
|
368
|
+
|
|
369
|
+
client = create_client(binary_path=str(fake_binary))
|
|
370
|
+
assert client is not None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_create_client_with_extra_env(tmp_path):
|
|
374
|
+
"""Test create_client with extra environment variables."""
|
|
375
|
+
from helm_mcp.client import create_client
|
|
376
|
+
|
|
377
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
378
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
379
|
+
fake_binary.chmod(0o755)
|
|
380
|
+
|
|
381
|
+
client = create_client(
|
|
382
|
+
binary_path=str(fake_binary),
|
|
383
|
+
env={"EXTRA_VAR": "extra"},
|
|
384
|
+
)
|
|
385
|
+
assert client is not None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_create_client_binary_not_found():
|
|
389
|
+
"""Test create_client raises when binary not found."""
|
|
390
|
+
from helm_mcp.client import create_client
|
|
391
|
+
|
|
392
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
393
|
+
env["PATH"] = "/nonexistent"
|
|
394
|
+
|
|
395
|
+
with patch.dict(os.environ, env, clear=True), pytest.raises(FileNotFoundError):
|
|
396
|
+
create_client()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
# CLI module
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_cli_module_exists():
|
|
405
|
+
"""Test that CLI entry point module exists."""
|
|
406
|
+
from helm_mcp import cli
|
|
407
|
+
|
|
408
|
+
assert hasattr(cli, "main")
|
|
409
|
+
assert callable(cli.main)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_cli_help(capsys):
|
|
413
|
+
"""Test CLI --help exits cleanly."""
|
|
414
|
+
from helm_mcp.cli import main
|
|
415
|
+
|
|
416
|
+
with (
|
|
417
|
+
pytest.raises(SystemExit) as exc_info,
|
|
418
|
+
patch("sys.argv", ["helm-mcp-python", "--help"]),
|
|
419
|
+
):
|
|
420
|
+
main()
|
|
421
|
+
assert exc_info.value.code == 0
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_cli_invalid_transport(capsys):
|
|
425
|
+
"""Test CLI rejects invalid transport."""
|
|
426
|
+
from helm_mcp.cli import main
|
|
427
|
+
|
|
428
|
+
with (
|
|
429
|
+
pytest.raises(SystemExit) as exc_info,
|
|
430
|
+
patch("sys.argv", ["helm-mcp-python", "--transport", "invalid"]),
|
|
431
|
+
):
|
|
432
|
+
main()
|
|
433
|
+
assert exc_info.value.code != 0
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_cli_binary_not_found(capsys):
|
|
437
|
+
"""Test CLI exits with error when binary not found."""
|
|
438
|
+
from helm_mcp.cli import main
|
|
439
|
+
|
|
440
|
+
env = {k: v for k, v in os.environ.items() if k != "HELM_MCP_BINARY"}
|
|
441
|
+
env["PATH"] = "/nonexistent"
|
|
442
|
+
|
|
443
|
+
with (
|
|
444
|
+
patch.dict(os.environ, env, clear=True),
|
|
445
|
+
patch("sys.argv", ["helm-mcp-python"]),
|
|
446
|
+
pytest.raises(SystemExit) as exc_info,
|
|
447
|
+
):
|
|
448
|
+
main()
|
|
449
|
+
assert exc_info.value.code == 1
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_cli_stdio_transport(tmp_path):
|
|
453
|
+
"""Test CLI with stdio transport calls server.run()."""
|
|
454
|
+
from unittest.mock import MagicMock
|
|
455
|
+
|
|
456
|
+
from helm_mcp.cli import main
|
|
457
|
+
|
|
458
|
+
mock_server = MagicMock()
|
|
459
|
+
|
|
460
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
461
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
462
|
+
fake_binary.chmod(0o755)
|
|
463
|
+
|
|
464
|
+
with (
|
|
465
|
+
patch("sys.argv", ["helm-mcp-python", "--binary", str(fake_binary)]),
|
|
466
|
+
patch("helm_mcp.server.create_server", return_value=mock_server) as mock_create,
|
|
467
|
+
):
|
|
468
|
+
main()
|
|
469
|
+
|
|
470
|
+
mock_create.assert_called_once_with(binary_path=str(fake_binary))
|
|
471
|
+
mock_server.run.assert_called_once_with()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_cli_http_transport(tmp_path):
|
|
475
|
+
"""Test CLI with http transport passes host and port."""
|
|
476
|
+
from unittest.mock import MagicMock
|
|
477
|
+
|
|
478
|
+
from helm_mcp.cli import main
|
|
479
|
+
|
|
480
|
+
mock_server = MagicMock()
|
|
481
|
+
|
|
482
|
+
fake_binary = tmp_path / "helm-mcp"
|
|
483
|
+
fake_binary.write_text("#!/bin/sh\necho hello")
|
|
484
|
+
fake_binary.chmod(0o755)
|
|
485
|
+
|
|
486
|
+
with (
|
|
487
|
+
patch(
|
|
488
|
+
"sys.argv",
|
|
489
|
+
[
|
|
490
|
+
"helm-mcp-python",
|
|
491
|
+
"--binary",
|
|
492
|
+
str(fake_binary),
|
|
493
|
+
"--transport",
|
|
494
|
+
"http",
|
|
495
|
+
"--host",
|
|
496
|
+
"127.0.0.1",
|
|
497
|
+
"--port",
|
|
498
|
+
"9090",
|
|
499
|
+
],
|
|
500
|
+
),
|
|
501
|
+
patch("helm_mcp.server.create_server", return_value=mock_server),
|
|
502
|
+
):
|
|
503
|
+
main()
|
|
504
|
+
|
|
505
|
+
mock_server.run.assert_called_once_with(transport="http", host="127.0.0.1", port=9090)
|