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.
@@ -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
@@ -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
@@ -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)