@dockndevai/mcp-kubernetes 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/k8s/client.d.ts +47 -0
- package/dist/k8s/client.js +177 -0
- package/dist/k8s/client.js.map +1 -0
- package/dist/security.d.ts +61 -0
- package/dist/security.js +106 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/admin.d.ts +6 -0
- package/dist/tools/admin.js +78 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/read.d.ts +2 -0
- package/dist/tools/read.js +222 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/types.d.ts +41 -0
- package/dist/tools/types.js +21 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/write.d.ts +2 -0
- package/dist/tools/write.js +148 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ankit Verma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# mcp-kubernetes
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dockndevai/mcp-kubernetes/actions/workflows/ci.yml)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for **Kubernetes**. It lets an MCP-capable client (Claude Desktop, Claude Code, etc.) inspect and operate Kubernetes clusters across multiple contexts — with behaviour controlled entirely by flags.
|
|
7
|
+
|
|
8
|
+
The design goal is **safe by default**: it starts read-only, can be scoped to an allowlist of namespaces and contexts, protects system namespaces from mutation, and gates the dangerous operations (delete, apply, exec) behind explicit opt-ins.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Multi-cluster** — every tool accepts an optional `context`; scope which contexts are usable with an allowlist.
|
|
13
|
+
- **Access modes** — `read-only` → `read-write` → `admin`, layered so a mode never exposes tools above its level.
|
|
14
|
+
- **Security flags** — namespace allowlist, protected namespaces, context allowlist, plus independent opt-ins for delete / apply / exec, dry-run, and JSON audit logging (see below).
|
|
15
|
+
- **Standard auth** — uses your kube-config (or in-cluster service account). No credentials are stored by the server.
|
|
16
|
+
|
|
17
|
+
## Security model
|
|
18
|
+
|
|
19
|
+
| Concern | Flag | Default | Effect |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| What can the server do at all? | `K8S_MODE` | `read-only` | `read-only` exposes only reads; `read-write` adds mutations; `admin` adds destructive tools. Tools above the mode are **never registered**. |
|
|
22
|
+
| Which namespaces are in scope? | `K8S_NAMESPACE_ALLOWLIST` | *(all)* | When set, any operation on a namespace outside the list is refused. |
|
|
23
|
+
| Which namespaces are read-only forever? | `K8S_PROTECTED_NAMESPACES` | `kube-system,kube-public,kube-node-lease` | Can be read but never mutated or deleted, regardless of mode. |
|
|
24
|
+
| Which clusters are reachable? | `K8S_CONTEXT_ALLOWLIST` | *(all)* | When set, only these kube-config contexts may be targeted. |
|
|
25
|
+
| Can it delete? | `K8S_ALLOW_DELETE` | `false` | `delete_resource` needs this **and** admin mode. |
|
|
26
|
+
| Can it apply manifests? | `K8S_ALLOW_APPLY` | `false` | `apply_manifest` needs this **and** read-write mode. |
|
|
27
|
+
| Can it exec into pods? | `K8S_ALLOW_EXEC` | `false` | `exec_in_pod` needs this **and** admin mode; the tool isn't even registered otherwise. |
|
|
28
|
+
| Preview without touching the cluster | `K8S_DRY_RUN` | `false` | Write/admin tools validate + log intent, then return without calling the API. |
|
|
29
|
+
| Audit trail | `K8S_AUDIT_LOG` | `true` | Emits a JSON line to stderr per guarded operation (`ALLOW` / `DENY` / `DRY_RUN`). |
|
|
30
|
+
|
|
31
|
+
The layers are independent — e.g. `admin` mode with all three opt-ins `false` can restart and scale deployments but can neither delete resources nor exec into pods.
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
**Read** (`read-only`+): `list_contexts`, `list_namespaces`, `list_pods`, `get_pod`, `get_pod_logs`, `list_deployments`, `list_services`, `list_nodes`, `list_events`, `get_resource`
|
|
36
|
+
|
|
37
|
+
**Write** (`read-write`+): `scale_deployment`, `restart_deployment`, `set_deployment_image`, `create_namespace`, `apply_manifest` (needs `K8S_ALLOW_APPLY`)
|
|
38
|
+
|
|
39
|
+
**Admin** (`admin`): `delete_resource` (needs `K8S_ALLOW_DELETE`), `exec_in_pod` (needs `K8S_ALLOW_EXEC`)
|
|
40
|
+
|
|
41
|
+
## Use with your MCP client
|
|
42
|
+
|
|
43
|
+
Works with Claude Code, Claude Desktop, Cursor, OpenAI Codex CLI, Windsurf, VS Code (Copilot), and any other MCP client — see **[docs/CLIENTS.md](docs/CLIENTS.md)** for per-client setup.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Run with Claude Desktop / Claude Code
|
|
53
|
+
|
|
54
|
+
Add to your MCP client configuration:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"kubernetes": {
|
|
60
|
+
"command": "node",
|
|
61
|
+
"args": ["/absolute/path/to/mcp-kubernetes/dist/index.js"],
|
|
62
|
+
"env": {
|
|
63
|
+
"KUBECONFIG_PATH": "/Users/you/.kube/config",
|
|
64
|
+
"K8S_MODE": "read-only",
|
|
65
|
+
"K8S_CONTEXT_ALLOWLIST": "staging",
|
|
66
|
+
"K8S_NAMESPACE_ALLOWLIST": "app,web"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Bump `K8S_MODE` to `read-write` for scaling/restarts, and to `admin` (plus the relevant `K8S_ALLOW_*` flag) only when you intend to allow deletes or exec.
|
|
74
|
+
|
|
75
|
+
## Develop
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run dev # watch mode
|
|
79
|
+
npm test # unit tests for the security policy
|
|
80
|
+
npm run typecheck
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Publishing
|
|
84
|
+
|
|
85
|
+
This server ships a [`server.json`](server.json) for the official MCP registry and an [`mcpName`](package.json) for npm ownership validation. See **[PUBLISHING.md](PUBLISHING.md)** for publishing to npm and listing on the MCP registry, Smithery, Glama, Cursor, and PulseMCP.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration from environment variables. Cluster connection details come
|
|
3
|
+
* from a standard kube-config (KUBECONFIG or ~/.kube/config, or in-cluster).
|
|
4
|
+
*/
|
|
5
|
+
import type { SecurityConfig } from "./security.js";
|
|
6
|
+
export interface KubeConnection {
|
|
7
|
+
/** Explicit path to a kube-config file. Empty = default resolution. */
|
|
8
|
+
kubeconfigPath?: string;
|
|
9
|
+
/** Load in-cluster config (service account) instead of a kube-config file. */
|
|
10
|
+
inCluster: boolean;
|
|
11
|
+
/** Default context to use when a tool call omits one. Empty = current-context. */
|
|
12
|
+
defaultContext?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AppConfig {
|
|
15
|
+
connection: KubeConnection;
|
|
16
|
+
security: SecurityConfig;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadConfig(): AppConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DEFAULT_PROTECTED = ["kube-system", "kube-public", "kube-node-lease"];
|
|
2
|
+
function bool(name, fallback) {
|
|
3
|
+
const v = process.env[name];
|
|
4
|
+
if (v === undefined || v === "")
|
|
5
|
+
return fallback;
|
|
6
|
+
return ["1", "true", "yes", "on"].includes(v.toLowerCase());
|
|
7
|
+
}
|
|
8
|
+
function list(name) {
|
|
9
|
+
const v = process.env[name];
|
|
10
|
+
if (!v)
|
|
11
|
+
return [];
|
|
12
|
+
return v
|
|
13
|
+
.split(",")
|
|
14
|
+
.map((s) => s.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
function parseMode() {
|
|
18
|
+
const raw = (process.env.K8S_MODE ?? "read-only").toLowerCase();
|
|
19
|
+
if (raw === "read-only" || raw === "read-write" || raw === "admin")
|
|
20
|
+
return raw;
|
|
21
|
+
throw new Error(`Invalid K8S_MODE '${raw}'. Expected one of: read-only, read-write, admin.`);
|
|
22
|
+
}
|
|
23
|
+
export function loadConfig() {
|
|
24
|
+
const protectedNs = list("K8S_PROTECTED_NAMESPACES");
|
|
25
|
+
return {
|
|
26
|
+
connection: {
|
|
27
|
+
kubeconfigPath: process.env.KUBECONFIG_PATH || undefined,
|
|
28
|
+
inCluster: bool("K8S_IN_CLUSTER", false),
|
|
29
|
+
defaultContext: process.env.K8S_CONTEXT || undefined,
|
|
30
|
+
},
|
|
31
|
+
security: {
|
|
32
|
+
mode: parseMode(),
|
|
33
|
+
namespaceAllowlist: list("K8S_NAMESPACE_ALLOWLIST"),
|
|
34
|
+
protectedNamespaces: protectedNs.length ? protectedNs : DEFAULT_PROTECTED,
|
|
35
|
+
contextAllowlist: list("K8S_CONTEXT_ALLOWLIST"),
|
|
36
|
+
allowDelete: bool("K8S_ALLOW_DELETE", false),
|
|
37
|
+
allowApply: bool("K8S_ALLOW_APPLY", false),
|
|
38
|
+
allowExec: bool("K8S_ALLOW_EXEC", false),
|
|
39
|
+
dryRun: bool("K8S_DRY_RUN", false),
|
|
40
|
+
auditLog: bool("K8S_AUDIT_LOG", true),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoBA,MAAM,iBAAiB,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,iBAAiB,CAAC,CAAC;AAE5E,SAAS,IAAI,CAAC,IAAY,EAAE,QAAiB;IAC3C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACjD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,IAAI,CAAC,IAAY;IACxB,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,OAAO,CAAC;SACL,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,SAAS;IAChB,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IAChE,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,OAAO;QAAE,OAAO,GAAG,CAAC;IAC/E,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,mDAAmD,CAAC,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,WAAW,GAAG,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACrD,OAAO;QACL,UAAU,EAAE;YACV,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,SAAS;YACxD,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC;YACxC,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,SAAS;SACrD;QACD,QAAQ,EAAE;YACR,IAAI,EAAE,SAAS,EAAE;YACjB,kBAAkB,EAAE,IAAI,CAAC,yBAAyB,CAAC;YACnD,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,iBAAiB;YACzE,gBAAgB,EAAE,IAAI,CAAC,uBAAuB,CAAC;YAC/C,WAAW,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC;YAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC;YAC1C,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC;YACxC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC;YAClC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC;SACtC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { buildServer } from "./server.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
let config;
|
|
7
|
+
try {
|
|
8
|
+
config = loadConfig();
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
process.stderr.write(`[kubernetes-mcp] Configuration error: ${err.message}\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
let built;
|
|
15
|
+
try {
|
|
16
|
+
built = buildServer(config);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
process.stderr.write(`[kubernetes-mcp] Failed to load kube-config: ${err.message}\n` +
|
|
20
|
+
"Check KUBECONFIG_PATH / K8S_IN_CLUSTER and that a valid kube-config is present.\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
process.stderr.write(`[kubernetes-mcp] Starting in '${config.security.mode}' mode` +
|
|
24
|
+
`${config.security.dryRun ? " (DRY RUN)" : ""}. ` +
|
|
25
|
+
`${built.enabled.length} tools enabled: ${built.enabled.join(", ")}\n`);
|
|
26
|
+
const transport = new StdioServerTransport();
|
|
27
|
+
await built.server.connect(transport);
|
|
28
|
+
}
|
|
29
|
+
main().catch((err) => {
|
|
30
|
+
process.stderr.write(`[kubernetes-mcp] Fatal: ${err.stack ?? err}\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,UAAU,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yCAA0C,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;QAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gDAAiD,GAAa,CAAC,OAAO,IAAI;YACxE,mFAAmF,CACtF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCAAiC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ;QAC3D,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI;QACjD,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,mBAAmB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACzE,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AACxC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA4B,GAAa,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;IACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type KubernetesObject } from "@kubernetes/client-node";
|
|
2
|
+
import type { KubeConnection } from "../config.js";
|
|
3
|
+
export interface ExecResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number | null;
|
|
7
|
+
}
|
|
8
|
+
export declare class K8sClient {
|
|
9
|
+
private readonly base;
|
|
10
|
+
constructor(conn: KubeConnection);
|
|
11
|
+
/** Names of all contexts in the loaded kube-config. */
|
|
12
|
+
listContextNames(): {
|
|
13
|
+
name: string;
|
|
14
|
+
cluster: string;
|
|
15
|
+
current: boolean;
|
|
16
|
+
}[];
|
|
17
|
+
currentContext(): string;
|
|
18
|
+
/** Return a KubeConfig scoped to a specific context (or the default). */
|
|
19
|
+
private scoped;
|
|
20
|
+
private core;
|
|
21
|
+
private apps;
|
|
22
|
+
private objects;
|
|
23
|
+
listNamespaces(context?: string): Promise<import("@kubernetes/client-node").V1NamespaceList>;
|
|
24
|
+
listPods(namespace: string, context?: string): Promise<import("@kubernetes/client-node").V1PodList>;
|
|
25
|
+
getPod(name: string, namespace: string, context?: string): Promise<import("@kubernetes/client-node").V1Pod>;
|
|
26
|
+
getPodLogs(name: string, namespace: string, opts: {
|
|
27
|
+
container?: string;
|
|
28
|
+
tailLines?: number;
|
|
29
|
+
previous?: boolean;
|
|
30
|
+
}, context?: string): Promise<string>;
|
|
31
|
+
listDeployments(namespace: string, context?: string): Promise<import("@kubernetes/client-node").V1DeploymentList>;
|
|
32
|
+
listServices(namespace: string, context?: string): Promise<import("@kubernetes/client-node").V1ServiceList>;
|
|
33
|
+
listNodes(context?: string): Promise<import("@kubernetes/client-node").V1NodeList>;
|
|
34
|
+
listEvents(namespace: string, context?: string): Promise<import("@kubernetes/client-node").CoreV1EventList>;
|
|
35
|
+
getObject(apiVersion: string, kind: string, name: string, namespace: string | undefined, context?: string): Promise<KubernetesObject>;
|
|
36
|
+
scaleDeployment(name: string, namespace: string, replicas: number, context?: string): Promise<import("@kubernetes/client-node").V1Deployment>;
|
|
37
|
+
restartDeployment(name: string, namespace: string, context?: string): Promise<import("@kubernetes/client-node").V1Deployment>;
|
|
38
|
+
setDeploymentImage(name: string, namespace: string, container: string, image: string, context?: string): Promise<import("@kubernetes/client-node").V1Deployment>;
|
|
39
|
+
createNamespace(name: string, context?: string): Promise<import("@kubernetes/client-node").V1Namespace>;
|
|
40
|
+
/** Create the object if absent, otherwise replace it (preserving resourceVersion). */
|
|
41
|
+
applyObject(obj: KubernetesObject, context?: string): Promise<{
|
|
42
|
+
action: "created" | "replaced";
|
|
43
|
+
}>;
|
|
44
|
+
deleteObject(apiVersion: string, kind: string, name: string, namespace: string | undefined, context?: string): Promise<import("@kubernetes/client-node").V1Status>;
|
|
45
|
+
/** Execute a command in a pod container and collect stdout/stderr. */
|
|
46
|
+
execInPod(name: string, namespace: string, container: string, command: string[], context?: string): Promise<ExecResult>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper over @kubernetes/client-node. Handles kube-config loading,
|
|
3
|
+
* per-call context selection, and the handful of operations the tools need.
|
|
4
|
+
*/
|
|
5
|
+
import { PassThrough } from "node:stream";
|
|
6
|
+
import { AppsV1Api, CoreV1Api, Exec, KubeConfig, KubernetesObjectApi, } from "@kubernetes/client-node";
|
|
7
|
+
export class K8sClient {
|
|
8
|
+
base;
|
|
9
|
+
constructor(conn) {
|
|
10
|
+
this.base = new KubeConfig();
|
|
11
|
+
if (conn.inCluster) {
|
|
12
|
+
this.base.loadFromCluster();
|
|
13
|
+
}
|
|
14
|
+
else if (conn.kubeconfigPath) {
|
|
15
|
+
this.base.loadFromFile(conn.kubeconfigPath);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.base.loadFromDefault();
|
|
19
|
+
}
|
|
20
|
+
if (conn.defaultContext) {
|
|
21
|
+
this.base.setCurrentContext(conn.defaultContext);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Names of all contexts in the loaded kube-config. */
|
|
25
|
+
listContextNames() {
|
|
26
|
+
const current = this.base.getCurrentContext();
|
|
27
|
+
return this.base.getContexts().map((c) => ({
|
|
28
|
+
name: c.name,
|
|
29
|
+
cluster: c.cluster,
|
|
30
|
+
current: c.name === current,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
currentContext() {
|
|
34
|
+
return this.base.getCurrentContext();
|
|
35
|
+
}
|
|
36
|
+
/** Return a KubeConfig scoped to a specific context (or the default). */
|
|
37
|
+
scoped(context) {
|
|
38
|
+
if (!context)
|
|
39
|
+
return this.base;
|
|
40
|
+
const kc = new KubeConfig();
|
|
41
|
+
kc.loadFromString(this.base.exportConfig());
|
|
42
|
+
kc.setCurrentContext(context);
|
|
43
|
+
return kc;
|
|
44
|
+
}
|
|
45
|
+
core(context) {
|
|
46
|
+
return this.scoped(context).makeApiClient(CoreV1Api);
|
|
47
|
+
}
|
|
48
|
+
apps(context) {
|
|
49
|
+
return this.scoped(context).makeApiClient(AppsV1Api);
|
|
50
|
+
}
|
|
51
|
+
objects(context) {
|
|
52
|
+
return KubernetesObjectApi.makeApiClient(this.scoped(context));
|
|
53
|
+
}
|
|
54
|
+
// --- Reads -----------------------------------------------------------------
|
|
55
|
+
listNamespaces(context) {
|
|
56
|
+
return this.core(context).listNamespace();
|
|
57
|
+
}
|
|
58
|
+
listPods(namespace, context) {
|
|
59
|
+
return this.core(context).listNamespacedPod({ namespace });
|
|
60
|
+
}
|
|
61
|
+
getPod(name, namespace, context) {
|
|
62
|
+
return this.core(context).readNamespacedPod({ name, namespace });
|
|
63
|
+
}
|
|
64
|
+
getPodLogs(name, namespace, opts, context) {
|
|
65
|
+
return this.core(context).readNamespacedPodLog({
|
|
66
|
+
name,
|
|
67
|
+
namespace,
|
|
68
|
+
container: opts.container,
|
|
69
|
+
tailLines: opts.tailLines,
|
|
70
|
+
previous: opts.previous,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
listDeployments(namespace, context) {
|
|
74
|
+
return this.apps(context).listNamespacedDeployment({ namespace });
|
|
75
|
+
}
|
|
76
|
+
listServices(namespace, context) {
|
|
77
|
+
return this.core(context).listNamespacedService({ namespace });
|
|
78
|
+
}
|
|
79
|
+
listNodes(context) {
|
|
80
|
+
return this.core(context).listNode();
|
|
81
|
+
}
|
|
82
|
+
listEvents(namespace, context) {
|
|
83
|
+
return this.core(context).listNamespacedEvent({ namespace });
|
|
84
|
+
}
|
|
85
|
+
getObject(apiVersion, kind, name, namespace, context) {
|
|
86
|
+
return this.objects(context).read({ apiVersion, kind, metadata: { name, namespace } });
|
|
87
|
+
}
|
|
88
|
+
// --- Writes ----------------------------------------------------------------
|
|
89
|
+
async scaleDeployment(name, namespace, replicas, context) {
|
|
90
|
+
const apps = this.apps(context);
|
|
91
|
+
const dep = await apps.readNamespacedDeployment({ name, namespace });
|
|
92
|
+
if (!dep.spec)
|
|
93
|
+
throw new Error(`Deployment '${name}' has no spec.`);
|
|
94
|
+
dep.spec.replicas = replicas;
|
|
95
|
+
return apps.replaceNamespacedDeployment({ name, namespace, body: dep });
|
|
96
|
+
}
|
|
97
|
+
async restartDeployment(name, namespace, context) {
|
|
98
|
+
const apps = this.apps(context);
|
|
99
|
+
const dep = await apps.readNamespacedDeployment({ name, namespace });
|
|
100
|
+
if (!dep.spec)
|
|
101
|
+
throw new Error(`Deployment '${name}' has no spec.`);
|
|
102
|
+
dep.spec.template.metadata = dep.spec.template.metadata ?? {};
|
|
103
|
+
dep.spec.template.metadata.annotations = {
|
|
104
|
+
...(dep.spec.template.metadata.annotations ?? {}),
|
|
105
|
+
"kubectl.kubernetes.io/restartedAt": new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
return apps.replaceNamespacedDeployment({ name, namespace, body: dep });
|
|
108
|
+
}
|
|
109
|
+
async setDeploymentImage(name, namespace, container, image, context) {
|
|
110
|
+
const apps = this.apps(context);
|
|
111
|
+
const dep = await apps.readNamespacedDeployment({ name, namespace });
|
|
112
|
+
const containers = dep.spec?.template?.spec?.containers ?? [];
|
|
113
|
+
const target = containers.find((c) => c.name === container);
|
|
114
|
+
if (!target) {
|
|
115
|
+
throw new Error(`Container '${container}' not found in deployment '${name}'. Containers: ${containers
|
|
116
|
+
.map((c) => c.name)
|
|
117
|
+
.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
target.image = image;
|
|
120
|
+
return apps.replaceNamespacedDeployment({ name, namespace, body: dep });
|
|
121
|
+
}
|
|
122
|
+
createNamespace(name, context) {
|
|
123
|
+
return this.core(context).createNamespace({ body: { metadata: { name } } });
|
|
124
|
+
}
|
|
125
|
+
/** Create the object if absent, otherwise replace it (preserving resourceVersion). */
|
|
126
|
+
async applyObject(obj, context) {
|
|
127
|
+
const api = this.objects(context);
|
|
128
|
+
try {
|
|
129
|
+
const existing = await api.read(obj);
|
|
130
|
+
const merged = {
|
|
131
|
+
...obj,
|
|
132
|
+
metadata: {
|
|
133
|
+
...obj.metadata,
|
|
134
|
+
resourceVersion: existing.metadata?.resourceVersion,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
await api.replace(merged);
|
|
138
|
+
return { action: "replaced" };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
await api.create(obj);
|
|
142
|
+
return { action: "created" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// --- Admin -----------------------------------------------------------------
|
|
146
|
+
deleteObject(apiVersion, kind, name, namespace, context) {
|
|
147
|
+
return this.objects(context).delete({ apiVersion, kind, metadata: { name, namespace } });
|
|
148
|
+
}
|
|
149
|
+
/** Execute a command in a pod container and collect stdout/stderr. */
|
|
150
|
+
async execInPod(name, namespace, container, command, context) {
|
|
151
|
+
const kc = this.scoped(context);
|
|
152
|
+
const exec = new Exec(kc);
|
|
153
|
+
const stdout = new PassThrough();
|
|
154
|
+
const stderr = new PassThrough();
|
|
155
|
+
const outChunks = [];
|
|
156
|
+
const errChunks = [];
|
|
157
|
+
stdout.on("data", (c) => outChunks.push(Buffer.from(c)));
|
|
158
|
+
stderr.on("data", (c) => errChunks.push(Buffer.from(c)));
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
exec
|
|
161
|
+
.exec(namespace, name, container, command, stdout, stderr, null, false, (status) => {
|
|
162
|
+
const exitCode = status?.status === "Success"
|
|
163
|
+
? 0
|
|
164
|
+
: typeof (status?.details?.causes?.[0]?.message) === "string"
|
|
165
|
+
? Number(status.details.causes[0].message) || 1
|
|
166
|
+
: 1;
|
|
167
|
+
resolve({
|
|
168
|
+
stdout: Buffer.concat(outChunks).toString("utf8"),
|
|
169
|
+
stderr: Buffer.concat(errChunks).toString("utf8"),
|
|
170
|
+
exitCode,
|
|
171
|
+
});
|
|
172
|
+
})
|
|
173
|
+
.catch(reject);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/k8s/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security policy engine.
|
|
3
|
+
*
|
|
4
|
+
* Flags decide which tools are registered (capability vs. access mode) and
|
|
5
|
+
* whether each individual call is allowed at runtime (namespace + context
|
|
6
|
+
* scoping, protected namespaces, destructive-op gating, exec gating, dry-run).
|
|
7
|
+
*
|
|
8
|
+
* Pure logic, no I/O, so it is fully unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
export type Capability = "read" | "write" | "admin";
|
|
11
|
+
export type AccessMode = "read-only" | "read-write" | "admin";
|
|
12
|
+
export interface SecurityConfig {
|
|
13
|
+
/** Highest capability the server may expose. */
|
|
14
|
+
mode: AccessMode;
|
|
15
|
+
/** If set, only these namespaces may be touched. Empty = all. */
|
|
16
|
+
namespaceAllowlist: string[];
|
|
17
|
+
/** Namespaces that can be read but never mutated or deleted. */
|
|
18
|
+
protectedNamespaces: string[];
|
|
19
|
+
/** If set, only these kube-config contexts may be used. Empty = all. */
|
|
20
|
+
contextAllowlist: string[];
|
|
21
|
+
/** Destructive delete_* operations require this to be true. */
|
|
22
|
+
allowDelete: boolean;
|
|
23
|
+
/** `apply_manifest` (arbitrary object create/replace) requires this. */
|
|
24
|
+
allowApply: boolean;
|
|
25
|
+
/** `exec_in_pod` (run commands in a container) requires this. */
|
|
26
|
+
allowExec: boolean;
|
|
27
|
+
/** Validate + log writes without sending them to the cluster. */
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
/** Emit a structured JSON audit line to stderr per guarded operation. */
|
|
30
|
+
auditLog: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class PolicyError extends Error {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
export interface GuardContext {
|
|
36
|
+
tool: string;
|
|
37
|
+
capability: Capability;
|
|
38
|
+
namespace?: string;
|
|
39
|
+
context?: string;
|
|
40
|
+
/** Destructive delete operation — needs allowDelete. */
|
|
41
|
+
destructive?: boolean;
|
|
42
|
+
/** Requires the apply opt-in. */
|
|
43
|
+
requiresApply?: boolean;
|
|
44
|
+
/** Requires the exec opt-in. */
|
|
45
|
+
requiresExec?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare class SecurityPolicy {
|
|
48
|
+
private readonly config;
|
|
49
|
+
constructor(config: SecurityConfig);
|
|
50
|
+
get mode(): AccessMode;
|
|
51
|
+
isCapabilityEnabled(capability: Capability): boolean;
|
|
52
|
+
isNamespaceAllowed(ns: string): boolean;
|
|
53
|
+
isNamespaceProtected(ns: string): boolean;
|
|
54
|
+
isContextAllowed(ctx: string): boolean;
|
|
55
|
+
/** Whether the exec tool should even be registered. */
|
|
56
|
+
get execEnabled(): boolean;
|
|
57
|
+
guard(ctx: GuardContext): {
|
|
58
|
+
dryRun: boolean;
|
|
59
|
+
};
|
|
60
|
+
private audit;
|
|
61
|
+
}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security policy engine.
|
|
3
|
+
*
|
|
4
|
+
* Flags decide which tools are registered (capability vs. access mode) and
|
|
5
|
+
* whether each individual call is allowed at runtime (namespace + context
|
|
6
|
+
* scoping, protected namespaces, destructive-op gating, exec gating, dry-run).
|
|
7
|
+
*
|
|
8
|
+
* Pure logic, no I/O, so it is fully unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
const MODE_RANK = {
|
|
11
|
+
"read-only": 0,
|
|
12
|
+
"read-write": 1,
|
|
13
|
+
admin: 2,
|
|
14
|
+
};
|
|
15
|
+
const CAPABILITY_RANK = {
|
|
16
|
+
read: 0,
|
|
17
|
+
write: 1,
|
|
18
|
+
admin: 2,
|
|
19
|
+
};
|
|
20
|
+
export class PolicyError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PolicyError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class SecurityPolicy {
|
|
27
|
+
config;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
get mode() {
|
|
32
|
+
return this.config.mode;
|
|
33
|
+
}
|
|
34
|
+
isCapabilityEnabled(capability) {
|
|
35
|
+
return CAPABILITY_RANK[capability] <= MODE_RANK[this.config.mode];
|
|
36
|
+
}
|
|
37
|
+
isNamespaceAllowed(ns) {
|
|
38
|
+
if (this.config.namespaceAllowlist.length === 0)
|
|
39
|
+
return true;
|
|
40
|
+
return this.config.namespaceAllowlist.includes(ns);
|
|
41
|
+
}
|
|
42
|
+
isNamespaceProtected(ns) {
|
|
43
|
+
return this.config.protectedNamespaces.includes(ns);
|
|
44
|
+
}
|
|
45
|
+
isContextAllowed(ctx) {
|
|
46
|
+
if (this.config.contextAllowlist.length === 0)
|
|
47
|
+
return true;
|
|
48
|
+
return this.config.contextAllowlist.includes(ctx);
|
|
49
|
+
}
|
|
50
|
+
/** Whether the exec tool should even be registered. */
|
|
51
|
+
get execEnabled() {
|
|
52
|
+
return this.isCapabilityEnabled("admin") && this.config.allowExec;
|
|
53
|
+
}
|
|
54
|
+
guard(ctx) {
|
|
55
|
+
if (!this.isCapabilityEnabled(ctx.capability)) {
|
|
56
|
+
this.audit(ctx, "DENY", `capability '${ctx.capability}' exceeds mode '${this.config.mode}'`);
|
|
57
|
+
throw new PolicyError(`Operation '${ctx.tool}' requires '${ctx.capability}' access but the server runs in '${this.config.mode}' mode.`);
|
|
58
|
+
}
|
|
59
|
+
if (ctx.context !== undefined && !this.isContextAllowed(ctx.context)) {
|
|
60
|
+
this.audit(ctx, "DENY", `context '${ctx.context}' not in allowlist`);
|
|
61
|
+
throw new PolicyError(`Context '${ctx.context}' is not in the configured allowlist (K8S_CONTEXT_ALLOWLIST).`);
|
|
62
|
+
}
|
|
63
|
+
if (ctx.namespace !== undefined) {
|
|
64
|
+
if (!this.isNamespaceAllowed(ctx.namespace)) {
|
|
65
|
+
this.audit(ctx, "DENY", `namespace '${ctx.namespace}' not in allowlist`);
|
|
66
|
+
throw new PolicyError(`Namespace '${ctx.namespace}' is not in the configured allowlist (K8S_NAMESPACE_ALLOWLIST).`);
|
|
67
|
+
}
|
|
68
|
+
if (ctx.capability !== "read" && this.isNamespaceProtected(ctx.namespace)) {
|
|
69
|
+
this.audit(ctx, "DENY", `namespace '${ctx.namespace}' is protected`);
|
|
70
|
+
throw new PolicyError(`Namespace '${ctx.namespace}' is protected (K8S_PROTECTED_NAMESPACES); mutations are refused.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (ctx.requiresApply && !this.config.allowApply) {
|
|
74
|
+
this.audit(ctx, "DENY", "apply not enabled");
|
|
75
|
+
throw new PolicyError(`Operation '${ctx.tool}' is disabled. Set K8S_ALLOW_APPLY=true to enable applying manifests.`);
|
|
76
|
+
}
|
|
77
|
+
if (ctx.requiresExec && !this.config.allowExec) {
|
|
78
|
+
this.audit(ctx, "DENY", "exec not enabled");
|
|
79
|
+
throw new PolicyError(`Operation '${ctx.tool}' is disabled. Set K8S_ALLOW_EXEC=true to enable exec into pods.`);
|
|
80
|
+
}
|
|
81
|
+
if (ctx.destructive && !this.config.allowDelete) {
|
|
82
|
+
this.audit(ctx, "DENY", "delete not enabled");
|
|
83
|
+
throw new PolicyError(`Destructive operation '${ctx.tool}' is disabled. Set K8S_ALLOW_DELETE=true to enable it.`);
|
|
84
|
+
}
|
|
85
|
+
const dryRun = ctx.capability !== "read" && this.config.dryRun;
|
|
86
|
+
this.audit(ctx, dryRun ? "DRY_RUN" : "ALLOW");
|
|
87
|
+
return { dryRun };
|
|
88
|
+
}
|
|
89
|
+
audit(ctx, decision, reason) {
|
|
90
|
+
if (!this.config.auditLog)
|
|
91
|
+
return;
|
|
92
|
+
const line = {
|
|
93
|
+
ts: new Date().toISOString(),
|
|
94
|
+
audit: "kubernetes-mcp",
|
|
95
|
+
decision,
|
|
96
|
+
tool: ctx.tool,
|
|
97
|
+
capability: ctx.capability,
|
|
98
|
+
context: ctx.context ?? null,
|
|
99
|
+
namespace: ctx.namespace ?? null,
|
|
100
|
+
destructive: ctx.destructive ?? false,
|
|
101
|
+
...(reason ? { reason } : {}),
|
|
102
|
+
};
|
|
103
|
+
process.stderr.write(`${JSON.stringify(line)}\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"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"}
|