mcpnertes
# Mcpnertes
[](LICENSE)
[](pyproject.toml)
[](https://modelcontextprotocol.io)
[](#tools)
Read-only Kubernetes MCP server. Every tool is GET/LIST-only, and every tool
passes through a config-driven policy (`config.toml`) before it touches the
cluster:
- **Namespace allowlist** — `namespaces.allow` defaults to `["*"]` (every
namespace). Restrict it to an explicit list to scope the server to specific
namespaces, and use `namespaces.deny` to carve out exceptions.
- **Resource blocklist** — `resources.block` defaults to `["Secret"]`.
Blocked kinds are refused by every tool, including the generic
`list_resource`/`get_resource` dynamic-client tools, so there's no way to
route around the block by asking a different tool.
Both checks happen in a single chokepoint (`mcpnertes.config.Policy`) before
any Kubernetes API call is made — see [`src/mcpnertes/config.py`](src/mcpnertes/config.py).
## Install
```bash
pip install -e .
```
## Run standalone
```bash
mcpnertes
```
## Use from an MCP client (Claude Desktop, Claude Code, etc.)
Add to the client's MCP server config. The policy (namespace allow/deny,
resource block) is set directly in `env` — no `config.toml` file needed.
`uvx` fetches and runs it from PyPI without a local install:
```json
{
"mcpServers": {
"mcpnertes": {
"command": "uvx",
"args": ["mcpnertes"],
"env": {
"UV_HTTP_TIMEOUT": "120",
"MCPNERTES_NAMESPACE_ALLOW": "*",
"MCPNERTES_NAMESPACE_DENY": "kube-system,cert-manager",
"MCPNERTES_RESOURCE_BLOCK": "Secret"
}
}
}
}
```
`args` must be plain `mcpnertes`, not `mcpnertes@latest` — `@latest` is
npm/npx syntax, not a valid `uv`/`pip` version specifier, and `uvx` will
hang trying to resolve it until the MCP client's connect timeout kills it.
Plain `mcpnertes` always resolves to the newest PyPI release.
`UV_HTTP_TIMEOUT` is optional but recommended on a cold cache: the first
`uvx` run downloads the `kubernetes` dependency (~4.4MiB) and `uv`'s
default 30s HTTP timeout can be too short on a slow connection, causing
the same "connection timed out" symptom. 120s comfortably covers a cold
install; subsequent runs use `uv`'s cache and start in milliseconds.
All three `MCPNERTES_*` env vars are optional and comma-separated. Omit
any of them to fall back to `config.toml` (if present) or the built-in
default (allow all namespaces, block `Secret`).
**Not yet on PyPI?** Point `uvx` at this checkout instead:
```json
{
"mcpServers": {
"mcpnertes": {
"command": "uvx",
"args": ["--from", "/absolute/path/to/Mcpnertes", "mcpnertes"],
"env": {
"MCPNERTES_NAMESPACE_ALLOW": "*",
"MCPNERTES_NAMESPACE_DENY": "kube-system,cert-manager",
"MCPNERTES_RESOURCE_BLOCK": "Secret"
}
}
}
}
```
## Configuring the policy
Two equivalent ways to set it — pick whichever fits how you're running the
server. If both are present, the env vars win field-by-field.
### Env vars (for `mcpServers` configs — no file needed)
| Variable | Format | Default |
|---|---|---|
| `MCPNERTES_NAMESPACE_ALLOW` | comma-separated, `*` = all | `*` |
| `MCPNERTES_NAMESPACE_DENY` | comma-separated | *(empty)* |
| `MCPNERTES_RESOURCE_BLOCK` | comma-separated, case-insensitive | `Secret` |
```json
"env": {
"MCPNERTES_NAMESPACE_ALLOW": "default,staging",
"MCPNERTES_NAMESPACE_DENY": "",
"MCPNERTES_RESOURCE_BLOCK": "Secret,ConfigMap,Ingress"
}
```
### `config.toml` (for standalone runs)
Resolution order (first match wins): `$MCPNERTES_CONFIG`, `./config.toml`
(current working directory), then the `config.toml` shipped next to the
installed package.
```toml
[namespaces]
# "*" = every namespace (default). Replace with an explicit list to scope
# the server to only those namespaces.
allow = ["*"]
# Namespaces to block even if matched by `allow`. `deny` always wins.
deny = []
[resources]
# Resource kinds always refused, regardless of which tool is called
# (including the generic list_resource/get_resource tools). Case-insensitive.
block = ["Secret"]
```
**Scope to specific namespaces:**
```toml
[namespaces]
allow = ["default", "staging"]
deny = []
```
**Allow everything except a couple of sensitive namespaces:**
```toml
[namespaces]
allow = ["*"]
deny = ["kube-system", "cert-manager"]
```
**Block additional resource kinds** (e.g. also hide ConfigMaps and Ingresses):
```toml
[resources]
block = ["Secret", "ConfigMap", "Ingress"]
```
## Tools
| Tool | Description |
|---|---|
| `list_pods` | List pods in a namespace or across all namespaces |
| `list_deployments` | List deployments in a namespace or across all namespaces |
| `get_pod_logs` | Get logs from a specific pod |
| `list_services` | List services in a namespace or across all namespaces |
| `list_namespaces` | List namespaces (filtered by the allowlist) |
| `get_events` | Get cluster events for a namespace or all namespaces |
| `get_logs` | Get logs from a pod/deployment/job/label selector |
| `list_nodes` | List cluster nodes |
| `list_resource` | List any resource kind (including CRDs) via the dynamic client |
| `get_resource` | Get a single resource of any kind by name |
| `list_api_resources` | Discover listable resource kinds the cluster exposes |
## Testing
```bash
pip install -e ".[dev]"
pytest -q
```
`tests/test_config.py` unit-tests the `Policy` allow/deny/block logic in
isolation. `tests/test_e2e.py` and `tests/test_e2e_stdio.py` are full
end-to-end tests with no mocks: they seed a real namespace/pod/deployment
/service/secret on whatever cluster your current kubeconfig context points
to, then drive the actual MCP server (in-process and as a real stdio
subprocess) against it. They auto-skip if no cluster is reachable.
## License
Apache-2.0 — see [LICENSE](LICENSE).
TDQS
Scored across 11 tools
There is notable overlap between list_pods, list_deployments, list_services, list_resource, and list_api_resources. list_resource and get_resource are generic dynamic-client tools that can overlap with the specific resource listers, though the specific ones are convenient. get_pod_logs and get_logs also overlap, with get_logs being a broader version of get_pod_logs.
Most tools follow a consistent list_<resource> or get_<resource> pattern. Minor deviations: list_resource and get_resource are generic rather than resource-specific, and list_api_resources is a discovery operation rather than listing actual resources, but the overall pattern is predictable.
11 tools is a reasonable count for a Kubernetes MCP server covering common read-only operations. It is slightly redundant because generic list_resource/get_resource could subsume the specific listers, but the count is not excessive.
The server is read-only by design, so it covers listing and getting resources, logs, events, and discovery. However, it lacks common read operations like describe/status for resources, and there is no way to get a single deployment or pod by name except through the generic get_resource, which is a minor gap.