From 13c13be6c256f87c5f71824a87f19d3d8f057cbf Mon Sep 17 00:00:00 2001 From: arkadeepsen Date: Tue, 7 Jul 2026 12:44:08 +0530 Subject: [PATCH 1/5] feat(cluster-diagnostics): Use the passed context when timeout is not set and the context has a deadline Signed-off-by: arkadeepsen --- .../nodesdebug/nodes_debug.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/cluster-diagnostics/nodesdebug/nodes_debug.go b/pkg/cluster-diagnostics/nodesdebug/nodes_debug.go index 7566f41fd..ee0190bfd 100644 --- a/pkg/cluster-diagnostics/nodesdebug/nodes_debug.go +++ b/pkg/cluster-diagnostics/nodesdebug/nodes_debug.go @@ -140,13 +140,23 @@ func (n *NodeDebug) NodesDebugExec( if debugImage == "" { debugImage = DefaultNodeDebugImage } + + // If timeout is not set and ctx has a timeout, use that, else use the default timeout. + // If timeout is set, then use it. + var timeoutCtx context.Context + var cancel context.CancelFunc if timeout <= 0 { - timeout = DefaultNodeDebugTimeout + if _, ok := ctx.Deadline(); ok { + timeoutCtx = ctx + } else { + timeoutCtx, cancel = context.WithTimeout(ctx, DefaultNodeDebugTimeout) + defer cancel() + } + } else { + timeoutCtx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() } - timeoutCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - exists, err := n.DoesNodeExist(timeoutCtx, nodeName) if err != nil { return "", "", fmt.Errorf("failed to check if node %s exists: %w", nodeName, err) From e7a18d2ce851c936130c3e6de2d68a3c9426a6a3 Mon Sep 17 00:00:00 2001 From: arkadeepsen Date: Thu, 23 Jul 2026 22:55:11 +0530 Subject: [PATCH 2/5] feat(cni-diagnostics): Add kernel and network-tools to cni-diagnostics toolset Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: arkadeepsen Co-authored-by: Cursor --- docs/openshift/README.md | 3 +- docs/openshift/cni-diagnostics.md | 602 ++++++++++++++++++ docs/openshift/ovn-kubernetes.md | 7 +- pkg/mcp/openshift_modules.go | 1 + .../toolsets-cni-diagnostics-tools.json | 387 +++++++++++ pkg/mcp/toolsets_test.go | 2 + .../cni-diagnostics/adapter/adapter.go | 36 ++ pkg/toolsets/cni-diagnostics/config/config.go | 73 +++ pkg/toolsets/cni-diagnostics/kernel/tools.go | 449 +++++++++++++ .../cni-diagnostics/network-tools/tools.go | 260 ++++++++ pkg/toolsets/cni-diagnostics/toolset.go | 45 ++ pkg/toolsets/cni-diagnostics/utils/utils.go | 175 +++++ .../pkg/kernel/mcp/conntrack.go | 122 ++++ .../ovn-kubernetes-mcp/pkg/kernel/mcp/ip.go | 129 ++++ .../pkg/kernel/mcp/iptables.go | 126 ++++ .../ovn-kubernetes-mcp/pkg/kernel/mcp/mcp.go | 206 ++++++ .../ovn-kubernetes-mcp/pkg/kernel/mcp/nft.go | 106 +++ .../ovn-kubernetes-mcp/pkg/kernel/mcp/util.go | 74 +++ .../pkg/kernel/types/types.go | 55 ++ .../pkg/network-tools/mcp/mcp.go | 97 +++ .../pkg/network-tools/mcp/pwru.go | 49 ++ .../pkg/network-tools/mcp/tcpdump.go | 87 +++ .../pkg/network-tools/mcp/utils.go | 49 ++ .../pkg/network-tools/types/manifest.go | 47 ++ .../utils/commandbuilder/command_builder.go | 40 ++ vendor/modules.txt | 5 + 26 files changed, 3229 insertions(+), 3 deletions(-) create mode 100644 docs/openshift/cni-diagnostics.md create mode 100644 pkg/mcp/testdata/toolsets-cni-diagnostics-tools.json create mode 100644 pkg/toolsets/cni-diagnostics/adapter/adapter.go create mode 100644 pkg/toolsets/cni-diagnostics/config/config.go create mode 100644 pkg/toolsets/cni-diagnostics/kernel/tools.go create mode 100644 pkg/toolsets/cni-diagnostics/network-tools/tools.go create mode 100644 pkg/toolsets/cni-diagnostics/toolset.go create mode 100644 pkg/toolsets/cni-diagnostics/utils/utils.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/conntrack.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/ip.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/iptables.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/mcp.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/nft.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/util.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types/types.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/mcp.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/pwru.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/tcpdump.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/utils.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types/manifest.go create mode 100644 vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder/command_builder.go diff --git a/docs/openshift/README.md b/docs/openshift/README.md index abd2325e9..6ceb8c213 100644 --- a/docs/openshift/README.md +++ b/docs/openshift/README.md @@ -10,7 +10,8 @@ This directory contains OpenShift-specific documentation for the OpenShift MCP S | **[NetEdge Toolset](NETEDGE.md)** | Guide for the Network Ingress & DNS troubleshooting tools | | **[ACM Setup](acm_setup.md)** | Setting up Advanced Cluster Management (ACM) and using the MCP Server with multi-cluster environments | | **[ACM with Keycloak Setup](acm_keycloak_setup.md)** | Setting up ACM with Keycloak-based OIDC authentication for secure multi-cluster access | -| **[OVN-Kubernetes Toolset](ovn-kubernetes.md)** | Guide for OVN-Kubernetes CNI network troubleshooting tools | +| **[OVN-Kubernetes Toolset](ovn-kubernetes.md)** | Guide for OVN-Kubernetes CNI network troubleshooting tools (use with [CNI Diagnostics](cni-diagnostics.md) for a complete toolkit) | +| **[CNI Diagnostics Toolset](cni-diagnostics.md)** | Tools for Container Network Interface (CNI) diagnostics and troubleshooting (enable with [OVN-Kubernetes](ovn-kubernetes.md) when troubleshooting OVN-Kubernetes) | ## Overview diff --git a/docs/openshift/cni-diagnostics.md b/docs/openshift/cni-diagnostics.md new file mode 100644 index 000000000..8629706bd --- /dev/null +++ b/docs/openshift/cni-diagnostics.md @@ -0,0 +1,602 @@ +# CNI Diagnostics Toolset + +The CNI Diagnostics toolset provides MCP tools for Container Network Interface (CNI) diagnostics and troubleshooting. + +## Overview + +This toolset includes 6 MCP tools organized into two categories: + +**Kernel Tools** — Diagnose kernel-level networking on nodes via privileged debug pods: + +| Tool | Description | +|------|-------------| +| `get-conntrack` | Connection tracking (conntrack) entries | +| `get-iptables` | IPv4/IPv6 packet filter rules (iptables/ip6tables) | +| `get-nft` | NFtables packet filtering and classification rules | +| `get-ip` | IP routing, interfaces, neighbours, and network namespaces (iproute2) | + +**Network Tools** — Advanced packet capture and eBPF tracing: + +| Tool | Description | +|------|-------------| +| `tcpdump` | Packet capture on nodes or inside pod network namespaces | +| `pwru` | eBPF-based kernel packet tracing (packet, where are you?) | + +All kernel tools and node-level network captures create ephemeral privileged debug pods on the target node using the configured container images. Pod-level `tcpdump` uses `pods/exec` on an existing pod instead. + +## Prerequisites + +### Cluster Requirements + +- **Nodes**: At least one worker node +- **Kernel Features**: + - Connection tracking (conntrack) + - iptables or nftables + - eBPF support for pwru (Linux kernel 4.18+) + +### RBAC Requirements + +The CNI Diagnostics toolset requires Kubernetes permissions to list nodes, create debug pods, and execute commands: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cni-diagnostics-mcp-user +rules: + # Required to verify nodes exist before creating debug pods + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list"] + # Required to create and manage privileged debug pods on nodes + - apiGroups: [""] + resources: ["pods"] + verbs: ["create", "get", "list", "delete"] + # Required to execute commands in debug pods (and pod-level tcpdump) + - apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] + # Required for debug pod log access + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cni-diagnostics-mcp-user-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cni-diagnostics-mcp-user +subjects: + - kind: ServiceAccount + name: kubernetes-mcp-server + namespace: default +``` + +### Security Considerations + +**Important security notes:** + +1. **Privileged access**: These tools create privileged debug pods with host network access +2. **Data sensitivity**: Output may contain network topology, IP addresses, and traffic patterns +3. **Resource impact**: Packet capture tools can impact node performance if used excessively +4. **Audit logging**: Tool invocations are recorded in Kubernetes audit logs + +**Recommended practices**: + +- Grant permissions only to trusted users +- Use namespace-scoped RoleBindings when possible +- Monitor audit logs for unexpected tool usage +- Configure `read_only = true` in server config to prevent destructive operations elsewhere in the server + +## Configuration + +### Enabling the Toolset + +Add `cni-diagnostics` to your toolsets configuration: + +```toml +# config.toml +toolsets = ["core", "config", "cni-diagnostics"] +``` + +When troubleshooting OVN-Kubernetes clusters, enable this toolset together with [`ovn-kubernetes`](ovn-kubernetes.md) so you have both CNI/kernel diagnostics and OVN/OVS layer tools: + +```toml +# config.toml +toolsets = ["core", "ovn-kubernetes", "cni-diagnostics"] +``` + +### Container Image Configuration + +Customize the container images used for debug pods: + +```toml +[toolset_configs.cni-diagnostics] +# Container image for kernel tools (conntrack, iptables, nft, ip) +# Default: nicolaka/netshoot:v0.16 +kernel_debug_image = "nicolaka/netshoot:v0.16" + +# Container image for tcpdump (node-level captures) +# Default: nicolaka/netshoot:v0.16 +tcpdump_image = "nicolaka/netshoot:v0.16" + +# Container image for pwru (eBPF packet tracing) +# Default: docker.io/cilium/pwru:v1.0.10 +pwru_image = "docker.io/cilium/pwru:v1.0.10" +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `kernel_debug_image` | string | `nicolaka/netshoot:v0.16` | Image for kernel tools (must include conntrack, iptables/ip6tables, nft, and ip utilities) | +| `tcpdump_image` | string | `nicolaka/netshoot:v0.16` | Image for node-level tcpdump captures (must include tcpdump) | +| `pwru_image` | string | `docker.io/cilium/pwru:v1.0.10` | Image for pwru (must include pwru and eBPF support) | + +When the toolset config section is omitted entirely, handlers fall back to the same default images at runtime. + +## Tools Reference + +Kernel tools (`get-conntrack`, `get-iptables`, `get-nft`, `get-ip`) create ephemeral debug pods on the target node. Network tools use debug pods for node-level operations (`tcpdump` with `target_type=node`, `pwru`) or `pods/exec` for pod-level `tcpdump` (`target_type=pod`). + +**Default behavior notes:** + +- **Kernel tools / `pwru` debug pod namespace** (`namespace` on kernel tools, `node_pod_namespace` on `pwru`): omitted or empty → `default` +- **`tcpdump` `namespace`**: required when `target_type=pod`; when `target_type=node`, omitted or empty → `default` (namespace for the node debug pod, not the capture target) +- **`head`**: omitted or `0` → first **100** lines (kernel tools only; `get-nft` applies the same limit at runtime) +- **`tail`**: omitted or `0` → not applied +- **`timeout_seconds`**: omitted or `0` → server default timeout (node debug operations fall back to **60 seconds** per `nodes_debug_exec` call when the MCP request context has no deadline). When set (up to **300**), that timeout applies to the **entire tool call** + +### `get-conntrack` + +Inspect the connection tracking table on a node. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `node` | Yes | — | Target node name | +| `namespace` | No | `default` | Namespace for the debug pod | +| `command` | No | `-L` | Conntrack operation: `-L`/`--dump`, `-C`/`--count`, or `-S`/`--stats` | +| `filter_parameters` | No | `—` (empty) | conntrack filter flags (e.g. `-s 10.244.1.10 -p tcp --dport 443`) | +| `head` | No | `100` | Return only the first N lines of output (omitted or `0` → 100) | +| `tail` | No | `—` (not applied) | Return only the last N lines of output (only applied when set to a positive value) | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +If the configured image lacks the `conntrack` binary, only `-L`/`--dump` is supported and output is read from `/proc/net/nf_conntrack`. + +### `get-iptables` + +List iptables or ip6tables rules on a node. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `node` | Yes | — | Target node name | +| `namespace` | No | `default` | Namespace for the debug pod | +| `table` | No | `filter` | Table name: `filter`, `nat`, `mangle`, `raw`, or `security` | +| `command` | No | `-L` | `-L`/`--list` or `-S`/`--list-rules` | +| `filter_parameters` | No | `—` (empty) | Additional iptables flags (e.g. `-v -n`, `-6` for IPv6) or a chain name (e.g. `FORWARD`) passed after `-L` | +| `head` | No | `100` | Return only the first N lines of output (omitted or `0` → 100) | +| `tail` | No | `—` (not applied) | Return only the last N lines of output (only applied when set to a positive value) | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +Use `-6` or `--ipv6` in `filter_parameters` to query ip6tables instead of iptables. When `filter_parameters` is omitted, all chains in the selected `table` are listed. + +### `get-nft` + +List nftables rules on a node. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `node` | Yes | — | Target node name | +| `namespace` | No | `default` | Namespace for the debug pod | +| `command` | Yes | — | One of: `list ruleset`, `list tables`, `list chains`, `list sets`, `list maps`, `list flowtables` | +| `address_families` | No | `—` (empty) | Address family filter: `ip`, `ip6`, `inet`, `arp`, `bridge`, or `netdev` | +| `head` | No | `100` | Return only the first N lines of output (omitted or `0` → 100 at runtime) | +| `tail` | No | `—` (not applied) | Return only the last N lines of output (only applied when set to a positive value) | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +### `get-ip` + +Run iproute2 `ip` subcommands on a node. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `node` | Yes | — | Target node name | +| `namespace` | No | `default` | Namespace for the debug pod | +| `command` | Yes | — | One of: `address show`, `link show`, `neighbour show`, `netns show`, `route show`, `rule show`, `vrf show`, `xfrm state list`, `xfrm policy list` | +| `options` | No | `—` (empty) | ip command options (e.g. `-4`, `-6`, `-d`, `-n `) | +| `filter_parameters` | No | `—` (empty) | Additional filter arguments passed to the subcommand | +| `head` | No | `100` | Return only the first N lines of output (omitted or `0` → 100) | +| `tail` | No | `—` (not applied) | Return only the last N lines of output (only applied when set to a positive value) | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +### `tcpdump` + +Capture network packets on a node or inside a pod. Node-level captures create an ephemeral debug pod; pod-level captures use `pods/exec` on an existing pod. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `target_type` | Yes | — | `node` or `pod` | +| `name` | Yes | — | Name of the target node (when `target_type=node`) or pod (when `target_type=pod`) | +| `namespace` | When `target_type=pod` | `default` (node captures only) | Namespace of the target pod, or namespace for the node debug pod when `target_type=node` | +| `container_name` | No | (pod default container) | Container within the pod (pod captures only) | +| `interface` | No | `—` (tcpdump default) | Network interface name or `any` | +| `packet_count` | No | `100` | Packets to capture (omitted or `0` → 100; maximum 1000) | +| `bpf_filter` | No | `—` (empty) | BPF filter expression | +| `snaplen` | No | `96` | Snapshot length in bytes (omitted or `0` → 96; maximum 1500) | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +When stderr is present, output is labeled with `-- stdout --` and `-- stderr --` sections. + +### `pwru` + +Trace packets through the kernel networking stack using eBPF on a node. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `node_name` | Yes | — | Node to run pwru on | +| `node_pod_namespace` | No | `default` | Namespace for the debug pod | +| `bpf_filter` | No | `—` (empty) | BPF filter expression | +| `output_limit_lines` | No | `100` | Maximum trace events to capture (omitted or `0` → 100; maximum 1000) | +| `timeout_seconds` | No | `—` | When omitted or `0`: server default timeout (see [default behavior notes](#tools-reference)). When set: timeout for the entire tool call (maximum 300) | + +When stderr is present, output is labeled with `-- stdout --` and `-- stderr --` sections (same as `tcpdump`). + +## Usage Examples + +### Connection Tracking (`get-conntrack`) + +**List active connections**: + +```json +{ + "name": "get-conntrack", + "arguments": { + "node": "worker-0", + "command": "-L" + } +} +``` + +**Filter connections to the API server**: + +```json +{ + "name": "get-conntrack", + "arguments": { + "node": "worker-0", + "command": "-L", + "filter_parameters": "-p tcp --dport 6443", + "head": 50 + } +} +``` + +**Show connection tracking statistics**: + +```json +{ + "name": "get-conntrack", + "arguments": { + "node": "worker-0", + "command": "-S" + } +} +``` + +### IPtables Rules (`get-iptables`) + +**List NAT rules**: + +```json +{ + "name": "get-iptables", + "arguments": { + "node": "worker-0", + "command": "-L", + "table": "nat" + } +} +``` + +**List IPv6 filter rules with verbose output**: + +```json +{ + "name": "get-iptables", + "arguments": { + "node": "worker-0", + "command": "-L", + "table": "filter", + "filter_parameters": "-6 -v -n" + } +} +``` + +### NFtables (`get-nft`) + +**List all rulesets**: + +```json +{ + "name": "get-nft", + "arguments": { + "node": "worker-0", + "command": "list ruleset" + } +} +``` + +**List tables for the inet family**: + +```json +{ + "name": "get-nft", + "arguments": { + "node": "worker-0", + "command": "list tables", + "address_families": "inet" + } +} +``` + +### IP Routing (`get-ip`) + +**Show all routing tables**: + +```json +{ + "name": "get-ip", + "arguments": { + "node": "worker-0", + "command": "route show", + "filter_parameters": "table all" + } +} +``` + +**Show network interfaces**: + +```json +{ + "name": "get-ip", + "arguments": { + "node": "worker-0", + "command": "link show" + } +} +``` + +**Show IPv4 addresses**: + +```json +{ + "name": "get-ip", + "arguments": { + "node": "worker-0", + "command": "address show", + "options": "-4" + } +} +``` + +### Packet Capture (`tcpdump`) + +**Capture TCP traffic on a node**: + +```json +{ + "name": "tcpdump", + "arguments": { + "target_type": "node", + "name": "worker-0", + "packet_count": 50, + "bpf_filter": "tcp and port 8080" + } +} +``` + +**Capture in a pod**: + +```json +{ + "name": "tcpdump", + "arguments": { + "target_type": "pod", + "name": "my-app-pod", + "namespace": "default", + "packet_count": 100, + "bpf_filter": "host 10.96.0.1" + } +} +``` + +### eBPF Packet Tracing (`pwru`) + +**Trace packets through the kernel**: + +```json +{ + "name": "pwru", + "arguments": { + "node_name": "worker-0", + "bpf_filter": "host 10.244.0.5 and tcp and dst port 443", + "output_limit_lines": 100 + } +} +``` + +## Common Diagnostic Workflows + +### Diagnose Pod Egress + +**Scenario**: A pod cannot reach a destination outside the cluster; inspect node routing, connection state, host filtering, and on-the-wire traffic without assuming how egress or SNAT is implemented + +```text +# 1. Review node routes toward the destination +get-ip: node="worker-0", command="route show", filter_parameters="table all" + +# 2. Check connection tracking for flows from the pod to the destination +get-conntrack: node="worker-0", command="-L", filter_parameters="-s -d " + +# 3. Review host packet filtering that may affect egress +get-nft: node="worker-0", command="list ruleset", address_families="inet" + +# 4. Capture traffic on the node toward the destination +tcpdump: target_type="node", name="worker-0", bpf_filter="host and host ", packet_count=50 +``` + +### Diagnose Inter-Pod Connectivity + +**Scenario**: Traffic between two pods is failing; inspect routing, connection state, the kernel packet path, and host filtering without assuming how pod networking is implemented + +```text +# 1. Review node routes on the source node +get-ip: node="worker-0", command="route show", filter_parameters="table all" + +# 2. Check connection tracking for flows between the pods +get-conntrack: node="worker-0", command="-L", filter_parameters="-s -d " + +# 3. Trace the packet path through the kernel networking stack +pwru: node_name="worker-0", bpf_filter="host and host ", output_limit_lines=200 + +# 4. Review host packet filtering if the trace stops at a filter hook +get-nft: node="worker-0", command="list ruleset", address_families="inet" +``` + +### Trace Dropped Pod Traffic + +**Scenario**: A pod on a node cannot reach a destination (another pod, a node, or an external host); trace where packets are dropped in the kernel networking stack without assuming how services or NAT are implemented + +```text +# 1. Review node routes toward the destination +get-ip: node="worker-0", command="route show", filter_parameters="table all" + +# 2. Check connection tracking for flows from the pod to the destination +get-conntrack: node="worker-0", command="-L", filter_parameters="-s -d " + +# 3. Trace the packet path through the kernel networking stack +pwru: node_name="worker-0", bpf_filter="host and host ", output_limit_lines=200 + +# 4. Review host packet filtering if the trace stops at a filter hook +get-nft: node="worker-0", command="list ruleset", address_families="inet" +``` + +## Troubleshooting + +### Common Issues + +#### "Forbidden" errors + +**Symptom**: `Error: pods is forbidden: User "..." cannot create resource "pods"` + +**Solution**: Grant the required RBAC permissions (see [RBAC Requirements](#rbac-requirements)) + +#### "Node not found" errors + +**Symptom**: `Error: node worker-0 does not exist` + +**Solution**: + +- Verify node name: `kubectl get nodes` +- Ensure the node is Ready +- Check node labels if using node selectors + +#### "Image pull failed" errors + +**Symptom**: `Failed to pull image: ... not found` + +**Solution**: + +- Verify image name in `[toolset_configs.cni-diagnostics]` +- Check image registry access from nodes +- Use `imagePullSecrets` if needed + +#### "Debug pod stuck in Pending" + +**Symptom**: Debug pod never reaches Running state + +**Solution**: + +- Check node capacity: `kubectl describe node ` +- Verify pod security policies allow privileged pods +- Check for taints/tolerations +- Review pod events: `kubectl describe pod ` + +#### "conntrack: command not found" + +**Symptom**: Error running conntrack commands + +**Solution**: + +- Use an image that includes the conntrack utility +- The default `nicolaka/netshoot:v0.16` image includes common networking tools +- Override with `kernel_debug_image` in config if needed + +#### "tcpdump: permission denied" + +**Symptom**: Cannot capture packets + +**Solution**: + +- Ensure the debug pod has `CAP_NET_RAW` +- Verify the node allows privileged pods +- Check pod security context and SCCs (OpenShift) + +### Debug Tips + +1. **Check debug pod logs**: + ```bash + kubectl logs -n + ``` + +2. **Verify image has required utilities**: + ```bash + kubectl run test --rm -it --image=nicolaka/netshoot:v0.16 -- which conntrack + ``` + +3. **Test RBAC permissions**: + ```bash + kubectl auth can-i create pods --as=system:serviceaccount:: + kubectl auth can-i create pods/exec --as=system:serviceaccount:: + ``` + +4. **Check cluster audit logs** for Forbidden events, pod creation, and exec events + +## Limitations + +- **Kernel tools** require ephemeral debug pod creation (adds latency) +- **pwru** requires eBPF support (Linux 4.18+) and runs only on nodes +- **Concurrent captures** may impact node performance +- **Debug pods** are ephemeral and cleaned up after use + +## Performance Considerations + +- Packet capture can impact node CPU if used heavily +- eBPF tracing (`pwru`) has lower overhead than `tcpdump` +- Use **BPF filters** to reduce captured packet volume +- Set **packet_count** and **output_limit_lines** to the minimum needed +- Use **head**/**tail** on kernel tools to limit output size +- Set **timeout_seconds** when the entire tool call may exceed the server default timeout (maximum 300) + +## Best Practices + +1. Use specific BPF filters to capture only relevant traffic +2. Limit packet counts and trace line counts to the minimum needed +3. Monitor resource usage when capturing on production nodes +4. Use `read_only = true` in server config when possible +5. Test diagnostic workflows in non-production clusters first + +## Related Documentation + +- [Configuration Reference](../configuration.md) +- [Core Toolset](../core.md) — for `pods_exec`, `resources_*` +- [OVN-Kubernetes Toolset](ovn-kubernetes.md) — enable alongside this toolset for complete OVN-Kubernetes troubleshooting + diff --git a/docs/openshift/ovn-kubernetes.md b/docs/openshift/ovn-kubernetes.md index 6202e3d3a..116d0b661 100644 --- a/docs/openshift/ovn-kubernetes.md +++ b/docs/openshift/ovn-kubernetes.md @@ -89,11 +89,13 @@ Add `ovn-kubernetes` to your toolsets configuration: ```toml # config.toml -toolsets = ["core", "ovn-kubernetes"] +toolsets = ["core", "ovn-kubernetes", "cni-diagnostics"] ``` The `core` toolset is required for discovering ovnkube-node pods via `pods_list`. +For a complete OVN-Kubernetes troubleshooting toolkit, also enable the [`cni-diagnostics`](cni-diagnostics.md) toolset. It provides kernel-level networking tools (conntrack, iptables, nftables, ip) and packet capture/tracing (`tcpdump`, `pwru`) that complement the OVN and OVS layer tools in this toolset. + ## Tools Reference OVN layer tools (`ovn_show`, `ovn_get`, `ovn_lflow_list`, `ovn_trace`) execute against an `ovnkube-node` pod. @@ -650,7 +652,7 @@ ovs_appctl: namespace="openshift-ovn-kubernetes", name="", action="dpctl/du #### "no record" errors from `ovn_get` -**Symptom**: `ovs-nbctl: no row "..." in table "..."` +**Symptom**: `ovn-nbctl: no row "..." in table "..."` **Solution**: @@ -661,3 +663,4 @@ ovs_appctl: namespace="openshift-ovn-kubernetes", name="", action="dpctl/du - [Configuration Reference](../configuration.md) - [Core Toolset](../README.md) — for `pods_list`, `pods_exec`, and other Kubernetes primitives +- [CNI Diagnostics Toolset](cni-diagnostics.md) — kernel networking and packet capture/tracing tools to use alongside this toolset for OVN-Kubernetes troubleshooting diff --git a/pkg/mcp/openshift_modules.go b/pkg/mcp/openshift_modules.go index 528e16f60..225cb4dd1 100644 --- a/pkg/mcp/openshift_modules.go +++ b/pkg/mcp/openshift_modules.go @@ -2,6 +2,7 @@ package mcp import ( _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cluster-diagnostics" + _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/mustgather" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/netedge" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/oadp" diff --git a/pkg/mcp/testdata/toolsets-cni-diagnostics-tools.json b/pkg/mcp/testdata/toolsets-cni-diagnostics-tools.json new file mode 100644 index 000000000..b1b98b163 --- /dev/null +++ b/pkg/mcp/testdata/toolsets-cni-diagnostics-tools.json @@ -0,0 +1,387 @@ +[ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Kernel Connection Tracking" + }, + "description": "Interact with the connection tracking system on a Kubernetes node. Lists, counts, or shows statistics for tracked connections. Connection tracking shows active network connections and their state (ESTABLISHED, TIME_WAIT, etc.).", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "command": { + "description": "These options specify the particular operation to perform. These options can only be used if configured image has 'conntrack' utility available.\n\t\t\t\t\t\t\t-L, --dump : List connection tracking table.\n\t\t\t\t\t\t\t-C, --count: Show the table counter.\n\t\t\t\t\t\t\t-S, --stats: Show the in-kernel connection tracking system statistics.", + "enum": [ + "-L", + "--dump", + "-C", + "--count", + "-S", + "--stats" + ], + "type": "string" + }, + "filter_parameters": { + "description": "These parameters are useful to filter certain entries from the whole table:\n\t\t\t\t\t\t\t-s, --src, --orig-src IP_ADDRESS : Match only entries whose source address in the original direction equals to mentioned IP.\n\t\t\t\t\t\t\t-d, --dst, --orig-dst IP_ADDRESS : Match only entries whose destination address in the original direction equals to mentioned IP.\n\t\t\t\t\t\t\t-p, --proto PROTO : Specify layer four (TCP, UDP, ...) protocol.\n\t\t\t\t\t\t\t--sport, --orig-port-src PORT : Source port in original direction.\n\t\t\t\t\t\t\t--dport, --orig-port-dst PORT : Destination port in original direction.", + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "minimum": 0, + "type": "integer" + }, + "namespace": { + "description": "Namespace of the debug pod from where conntrack entries are expected to be extracted (optional, defaults to 'default')", + "type": "string" + }, + "node": { + "description": "Name of the node from where conntrack entries are expected to be extracted", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 0, + "type": "integer" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "get-conntrack", + "title": "CNI-Diagnostics: Kernel Connection Tracking" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Kernel IP Command" + }, + "description": "Execute ip commands on a Kubernetes node to show routing, network devices, interfaces, and network namespaces. Part of the iproute2 suite for network configuration inspection.", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "command": { + "description": "These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below.\n - address show : protocol (IP or IPv6) address on a device.\n\t\t\t\t\t - link show : network device.\n\t\t\t\t\t - neighbour show : manage ARP or NDISC cache entries.\n\t\t\t\t\t - netns show : manage network namespaces.\n\t\t\t\t\t - route show : routing table entry.\n\t\t\t\t\t - rule show : rule in routing policy database.\n\t\t\t\t\t - vrf show : manage virtual routing and forwarding devices. \n\t\t\t\t\t - xfrm state list : show Security Association Database.\n\t\t\t\t\t - xfrm policy list : show Security Policy Database.", + "enum": [ + "address show", + "link show", + "neighbour show", + "netns show", + "route show", + "rule show", + "vrf show", + "xfrm state list", + "xfrm policy list" + ], + "type": "string" + }, + "filter_parameters": { + "description": "This allows to mention sub command to get more filtered data. Available sub command varies and supportability depends on what is \n already supported with 'ip' utility.", + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "minimum": 0, + "type": "integer" + }, + "namespace": { + "description": "Namespace of the debug pod on which ip command is expected to be executed (optional, defaults to 'default')", + "type": "string" + }, + "node": { + "description": "Name of the node on which ip command is expected to be executed", + "type": "string" + }, + "options": { + "description": "These options helps in providing more details or formattig output data.\n \t\t\t\t\t-d, -details : Output more detailed information.\n\t\t\t\t\t \t\t\t\t\t-4 : shortcut for -family inet.\n\t\t\t\t\t \t\t\t\t\t-6 : shortcut for -family inet6.\n\t\t\t\t\t \t\t\t\t\t-r, -resolve : use the system's name resolver to print DNS names instead of host addresses.\n\t\t\t\t\t \t\t\t\t\t-n, -netns \u003cNETNS\u003e : switches ip to the specified network namespace NETNS.\n\t\t\t\t\t -a, -all : executes specified command over all objects, it depends if command supports this option.", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 0, + "type": "integer" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "node", + "command" + ], + "type": "object" + }, + "name": "get-ip", + "title": "CNI-Diagnostics: Kernel IP Command" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Kernel IPtables" + }, + "description": "List packet filter rules using iptables or ip6tables on a Kubernetes node. Shows rules for specific tables (filter, nat, mangle, raw, security). Use this to inspect firewall rules, NAT configuration, and packet filtering on nodes.", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "command": { + "description": "These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below.\n\t\t\t\t\t\t\t-L, --list [chain] : List all rules in the selected chain. If no chain is selected, all chains are listed.\n\t\t\t\t\t\t\t-S, --list-rules [chain] : Print all rules in the selected chain. If no chain is selected, all chains are printed like iptables-save.", + "type": "string" + }, + "filter_parameters": { + "description": "These parameters are useful to filter certain entries from the whole table:\n\t\t\t\t\t\t\t-s, --source address[/mask] : Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address.\n\t\t\t\t\t\t\t-d, --destination address[/mask] : Destination specification.\n\t\t\t\t\t\t\t-v, --verbose\t\t\t\t\t : Verbose output.\n\t\t\t\t\t\t\t-n, --numeric : Numeric output. IP addresses and port numbers will be printed in numeric format.\n\t\t\t\t\t\t\t-p, --protocol protocol : The protocol of the rule or of the packet to check.\n\t\t\t\t\t\t\t-4, --ipv4 : IPv4\n\t\t\t\t\t\t\t-6, --ipv6 : IPv6", + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "minimum": 0, + "type": "integer" + }, + "namespace": { + "description": "Namespace of the debug pod from where packet filter rules are expected to be extracted (optional, defaults to 'default')", + "type": "string" + }, + "node": { + "description": "Name of the node from where packet filter rules are expected to be extracted", + "type": "string" + }, + "table": { + "description": "There are currently five independent tables (which tables are present at any time depends on the kernel configuration options and which modules are present).\n\t\t\t\t\t\t\tfilter\t: This is the default table\n\t\t\t\t\t\t\tnat \t: This table is consulted when a packet that creates a new connection is encountered.\n\t\t\t\t\t\t\tmangle\t: This table is used for specialized packet alteration.\n\t\t\t\t\t\t\traw \t: This table is used mainly for configuring exemptions from connection tracking in combination with the NOTRACK target.\n\t\t\t\t\t\t\tsecurity: This table is used for Mandatory Access Control (MAC) networking rules.", + "enum": [ + "filter", + "nat", + "mangle", + "raw", + "security" + ], + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 0, + "type": "integer" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "get-iptables", + "title": "CNI-Diagnostics: Kernel IPtables" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Kernel NFtables" + }, + "description": "List nftables packet filtering and classification rules on a Kubernetes node. nftables is the modern replacement for iptables. Use this to inspect firewall rules, packet filtering, and network address translation.", + "inputSchema": { + "properties": { + "address_families": { + "description": "Address families determine the type of packets which are processed. For each address family, the kernel contains so called hooks at specific stages of\n \t\t\t\t\t\t the packet processing paths, which invoke nftables if rules for these hooks exist.\n\t\t\t\t\t\t\t - ip IPv4 address family.\n - ip6 IPv6 address family.\n - inet Internet (IPv4/IPv6) address family.\n - arp ARP address family, handling IPv4 ARP packets.\n - bridge Bridge address family, handling packets which traverse a bridge device.\n - netdev Netdev address family, handling packets on ingress and egress.", + "enum": [ + "ip", + "ip6", + "inet", + "arp", + "bridge", + "netdev" + ], + "type": "string" + }, + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "command": { + "description": "These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below.\n \t\t\t\t\t- list ruleset : The ruleset keyword is used to identify the whole set of tables, chains, etc. Print the ruleset in human-readable format.\n\t\t\t\t\t\t\t\t\t\t- list tables : List all chains and rules of the specified table.\n\t\t\t\t\t\t\t\t\t\t- list chains : List all rules of the specified chain.\n\t\t\t\t\t\t\t\t\t\t- list sets : Display the elements in the specified set.\n\t\t\t\t\t\t\t\t\t\t- list maps : Display the elements in the specified map.\n\t\t\t\t\t\t\t\t\t\t- list flowtables: List all flowtables.", + "enum": [ + "list ruleset", + "list tables", + "list chains", + "list sets", + "list maps", + "list flowtables" + ], + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "minimum": 0, + "type": "integer" + }, + "namespace": { + "description": "Namespace of the debug pod from where packet filtering and classification rules are expected to be extracted (optional, defaults to 'default')", + "type": "string" + }, + "node": { + "description": "Name of the node from where packet filtering and classification rules are expected to be extracted", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 0, + "type": "integer" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "node", + "command" + ], + "type": "object" + }, + "name": "get-nft", + "title": "CNI-Diagnostics: Kernel NFtables" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Network eBPF Packet Tracing" + }, + "description": "Trace packets through the Linux kernel networking stack using eBPF. pwru (packet, where are you?) shows which kernel functions process a packet, helping debug packet drops and routing issues. Creates a specialized debug pod with eBPF capabilities.", + "inputSchema": { + "properties": { + "bpf_filter": { + "description": "BPF filter expression to match packets (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1')", + "type": "string" + }, + "node_name": { + "description": "Name of the node to run pwru on", + "type": "string" + }, + "node_pod_namespace": { + "description": "Namespace of the debug pod on which the command is expected to be executed (optional, defaults to 'default')", + "type": "string" + }, + "output_limit_lines": { + "description": "Maximum number of trace events to capture (default: 100, max: 1000)", + "maximum": 1000, + "minimum": 0, + "type": "integer" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "node_name" + ], + "type": "object" + }, + "name": "pwru", + "title": "CNI-Diagnostics: Network eBPF Packet Tracing" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": true, + "readOnlyHint": true, + "title": "CNI-Diagnostics: Network Packet Capture" + }, + "description": "Capture network packets on a node or inside a pod with BPF filtering. Creates a specialized debug pod for node-level captures. IMPORTANT: Use restrictive BPF filters and low packet counts to avoid performance impact. Maximum 1000 packets.", + "inputSchema": { + "properties": { + "bpf_filter": { + "description": "BPF filter expression (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1')", + "type": "string" + }, + "container_name": { + "description": "Name of the container in the pod when target_type is 'pod' (optional, uses default container if not specified)", + "type": "string" + }, + "interface": { + "description": "Network interface name or 'any' (optional, captures on all interfaces if not specified)", + "type": "string" + }, + "name": { + "description": "Name of the target (node or pod)", + "type": "string" + }, + "namespace": { + "description": "Namespace of the target (node or pod). Required when target_type is 'pod'. Optional when target_type is 'node' and defaults to 'default'.", + "type": "string" + }, + "packet_count": { + "description": "Number of packets to capture (default: 100, max: 1000)", + "maximum": 1000, + "minimum": 0, + "type": "integer" + }, + "snaplen": { + "description": "Snapshot length in bytes (default: 96, max: 1500). Use 96 for headers only, 1500 for full packets.", + "maximum": 1500, + "minimum": 0, + "type": "integer" + }, + "target_type": { + "description": "Capture target: 'node' (node-level) or 'pod' (pod network namespace)", + "enum": [ + "node", + "pod" + ], + "type": "string" + }, + "timeout_seconds": { + "description": "Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds.", + "maximum": 300, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "target_type", + "name" + ], + "type": "object" + }, + "name": "tcpdump", + "title": "CNI-Diagnostics: Network Packet Capture" + } +] diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go index bc9e35873..7104fdeb8 100644 --- a/pkg/mcp/toolsets_test.go +++ b/pkg/mcp/toolsets_test.go @@ -14,6 +14,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" clusterDiagnosticsToolset "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cluster-diagnostics" + cniDiagnosticsToolset "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm" @@ -226,6 +227,7 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() { &ovnkubernetes.Toolset{}, &tekton.Toolset{}, &clusterDiagnosticsToolset.Toolset{}, + &cniDiagnosticsToolset.Toolset{}, } for _, testCase := range testCases { s.Run("Toolset "+testCase.GetName(), func() { diff --git a/pkg/toolsets/cni-diagnostics/adapter/adapter.go b/pkg/toolsets/cni-diagnostics/adapter/adapter.go new file mode 100644 index 000000000..d646091d5 --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/adapter/adapter.go @@ -0,0 +1,36 @@ +package adapter + +import ( + "context" + "time" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/cluster-diagnostics/nodesdebug" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" +) + +// RunDebugNodeCommandFunc is an alias for the function type required by ovn-kubernetes-mcp. +// This matches ovnkmcp.RunDebugNodeCommandFuncType exactly. +type RunDebugNodeCommandFunc = func(ctx context.Context, namespace string, nodeName string, image string, command []string, hostPath string, mountPath string, timeout time.Duration) (string, string, error) + +// RunPodExecCommandFunc is an alias for the function type required by ovn-kubernetes-mcp. +// This matches the network-tools RunPodExecCommandFuncType exactly. +type RunPodExecCommandFunc = func(ctx context.Context, namespace, name, container string, command []string) (string, string, error) + +// NewRunDebugNodeCommand creates a RunDebugNodeCommandFunc that uses kubernetes-mcp-server's +// nodesdebug.NodeDebug to execute commands on nodes via privileged debug pods. +func NewRunDebugNodeCommand(k api.KubernetesClient) RunDebugNodeCommandFunc { + return func(ctx context.Context, namespace string, nodeName string, image string, command []string, hostPath string, mountPath string, timeout time.Duration) (string, string, error) { + nodeDebug := nodesdebug.NewNodeDebug(k) + return nodeDebug.NodesDebugExec(ctx, namespace, nodeName, image, command, hostPath, mountPath, timeout) + } +} + +// NewRunPodExecCommand creates a RunPodExecCommandFunc that uses kubernetes-mcp-server's +// kubernetes.Core to execute commands in existing pods. +func NewRunPodExecCommand(k api.KubernetesClient) RunPodExecCommandFunc { + return func(ctx context.Context, namespace, name, container string, command []string) (string, string, error) { + core := kubernetes.NewCore(k) + return core.PodsExec(ctx, namespace, name, container, command) + } +} diff --git a/pkg/toolsets/cni-diagnostics/config/config.go b/pkg/toolsets/cni-diagnostics/config/config.go new file mode 100644 index 000000000..acdb65034 --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/config/config.go @@ -0,0 +1,73 @@ +package config + +import ( + "context" + "fmt" + + "github.com/BurntSushi/toml" + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/config" +) + +const ( + DefaultNetshootImage = "nicolaka/netshoot:v0.16" + DefaultPwruImage = "docker.io/cilium/pwru:v1.0.10" +) + +// Config holds CNI Diagnostics toolset configuration +type Config struct { + // KernelDebugImage is the container image used for kernel tools (conntrack, iptables, nft, ip) + KernelDebugImage string `toml:"kernel_debug_image,omitempty"` + + // TcpdumpImage is the container image used for tcpdump packet capture + TcpdumpImage string `toml:"tcpdump_image,omitempty"` + + // PwruImage is the container image used for pwru eBPF packet tracing + PwruImage string `toml:"pwru_image,omitempty"` +} + +var _ api.ExtendedConfig = (*Config)(nil) + +// Validate validates the CNI Diagnostics toolset configuration +func (c *Config) Validate() error { + if c == nil { + return fmt.Errorf("cni-diagnostics config is nil") + } + + // Image names are validated at runtime when creating debug pods + // No specific validation needed here + return nil +} + +// GetConfig returns the CNI Diagnostics toolset configuration from the tool handler parameters +func GetConfig(params api.ToolHandlerParams) *Config { + if toolsetCfg, ok := params.GetToolsetConfig("cni-diagnostics"); ok { + if cniCfg, ok := toolsetCfg.(*Config); ok { + return cniCfg + } + } + return nil +} + +// cnidiagnosticsToolsetParser parses the CNI Diagnostics toolset configuration from TOML +func cnidiagnosticsToolsetParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { + var cfg Config + if err := md.PrimitiveDecode(primitive, &cfg); err != nil { + return nil, err + } + // Set default images if not provided + if cfg.KernelDebugImage == "" { + cfg.KernelDebugImage = DefaultNetshootImage + } + if cfg.TcpdumpImage == "" { + cfg.TcpdumpImage = DefaultNetshootImage + } + if cfg.PwruImage == "" { + cfg.PwruImage = DefaultPwruImage + } + return &cfg, nil +} + +func init() { + config.RegisterToolsetConfig("cni-diagnostics", cnidiagnosticsToolsetParser) +} diff --git a/pkg/toolsets/cni-diagnostics/kernel/tools.go b/pkg/toolsets/cni-diagnostics/kernel/tools.go new file mode 100644 index 000000000..5e9e25f6c --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/kernel/tools.go @@ -0,0 +1,449 @@ +package kernel + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/utils" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + ovnkkernelmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types" +) + +func InitKernelTools() []api.ServerTool { + return []api.ServerTool{ + initConntrackTool(), + initIPtablesTool(), + initNFTTool(), + initIPTool(), + } +} + +// initConntrackTool creates the get-conntrack tool. +func initConntrackTool() api.ServerTool { + props := map[string]*jsonschema.Schema{ + "node": { + Type: "string", + Description: "Name of the node from where conntrack entries are expected to be extracted", + }, + "namespace": { + Type: "string", + Description: "Namespace of the debug pod from where conntrack entries are expected to be extracted (optional, defaults to 'default')", + }, + "command": { + Type: "string", + Description: `These options specify the particular operation to perform. These options can only be used if configured image has 'conntrack' utility available. + -L, --dump : List connection tracking table. + -C, --count: Show the table counter. + -S, --stats: Show the in-kernel connection tracking system statistics.`, + Enum: []interface{}{"-L", "--dump", "-C", "--count", "-S", "--stats"}, + }, + "filter_parameters": { + Type: "string", + Description: `These parameters are useful to filter certain entries from the whole table: + -s, --src, --orig-src IP_ADDRESS : Match only entries whose source address in the original direction equals to mentioned IP. + -d, --dst, --orig-dst IP_ADDRESS : Match only entries whose destination address in the original direction equals to mentioned IP. + -p, --proto PROTO : Specify layer four (TCP, UDP, ...) protocol. + --sport, --orig-port-src PORT : Source port in original direction. + --dport, --orig-port-dst PORT : Destination port in original direction.`, + }, + } + utils.AddHeadTailProperties(props, fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnkkernelmcp.DefaultMaxOutputLines)) + utils.AddTimeoutSecondsProperty(props) + + return api.ServerTool{ + Tool: api.Tool{ + Name: "get-conntrack", + Description: "Interact with the connection tracking system on a Kubernetes node. Lists, counts, or shows statistics for tracked connections. Connection tracking shows active network connections and their state (ESTABLISHED, TIME_WAIT, etc.).", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{"node"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Kernel Connection Tracking", true), + }, + Handler: conntrackHandler, + } +} + +func conntrackHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + node := p.RequiredString("node") + namespace := p.OptionalString("namespace", "") + command := p.OptionalString("command", "") + filterParams := p.OptionalString("filter_parameters", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Create upstream parameters + upstreamParams := types.ListConntrackParams{ + CommonParams: types.CommonParams{ + Node: node, + Namespace: namespace, + }, + Command: command, + FilterParameters: filterParams, + } + + if result, err := utils.ValidateHeadTail(&upstreamParams.HeadTailParams, head, tail, applyTailFirst); result != nil { + return result, err + } + if result, err := utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); result != nil { + return result, err + } + + upstreamServer, err := utils.NewKernelMCPServer(params, utils.KernelDebugImage(params)) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.GetConntrack( + params.Context, + &mcp.CallToolRequest{}, // MCP request not used by handler + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get conntrack data: %w", err)), nil + } + + return api.NewToolCallResult(result.Data, nil), nil +} + +// initIPtablesTool creates the get-iptables tool. +func initIPtablesTool() api.ServerTool { + props := map[string]*jsonschema.Schema{ + "node": { + Type: "string", + Description: "Name of the node from where packet filter rules are expected to be extracted", + }, + "namespace": { + Type: "string", + Description: "Namespace of the debug pod from where packet filter rules are expected to be extracted (optional, defaults to 'default')", + }, + "table": { + Type: "string", + Description: `There are currently five independent tables (which tables are present at any time depends on the kernel configuration options and which modules are present). + filter : This is the default table + nat : This table is consulted when a packet that creates a new connection is encountered. + mangle : This table is used for specialized packet alteration. + raw : This table is used mainly for configuring exemptions from connection tracking in combination with the NOTRACK target. + security: This table is used for Mandatory Access Control (MAC) networking rules.`, + Enum: []interface{}{"filter", "nat", "mangle", "raw", "security"}, + }, + "command": { + Type: "string", + Description: `These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + -L, --list [chain] : List all rules in the selected chain. If no chain is selected, all chains are listed. + -S, --list-rules [chain] : Print all rules in the selected chain. If no chain is selected, all chains are printed like iptables-save.`, + }, + "filter_parameters": { + Type: "string", + Description: `These parameters are useful to filter certain entries from the whole table: + -s, --source address[/mask] : Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. + -d, --destination address[/mask] : Destination specification. + -v, --verbose : Verbose output. + -n, --numeric : Numeric output. IP addresses and port numbers will be printed in numeric format. + -p, --protocol protocol : The protocol of the rule or of the packet to check. + -4, --ipv4 : IPv4 + -6, --ipv6 : IPv6`, + }, + } + utils.AddHeadTailProperties(props, fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnkkernelmcp.DefaultMaxOutputLines)) + utils.AddTimeoutSecondsProperty(props) + + return api.ServerTool{ + Tool: api.Tool{ + Name: "get-iptables", + Description: "List packet filter rules using iptables or ip6tables on a Kubernetes node. Shows rules for specific tables (filter, nat, mangle, raw, security). Use this to inspect firewall rules, NAT configuration, and packet filtering on nodes.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{"node"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Kernel IPtables", true), + }, + Handler: iptablesHandler, + } +} + +func iptablesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + node := p.RequiredString("node") + namespace := p.OptionalString("namespace", "") + command := p.OptionalString("command", "-L") + table := p.OptionalString("table", "") + filterParams := p.OptionalString("filter_parameters", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Create upstream parameters + upstreamParams := types.ListIPTablesParams{ + CommonParams: types.CommonParams{ + Node: node, + Namespace: namespace, + }, + Table: table, + Command: command, + FilterParameters: filterParams, + } + + if result, err := utils.ValidateHeadTail(&upstreamParams.HeadTailParams, head, tail, applyTailFirst); result != nil { + return result, err + } + if result, err := utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); result != nil { + return result, err + } + + upstreamServer, err := utils.NewKernelMCPServer(params, utils.KernelDebugImage(params)) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.GetIptables( + params.Context, + &mcp.CallToolRequest{}, + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get iptables data: %w", err)), nil + } + + return api.NewToolCallResult(result.Data, nil), nil +} + +// initNFTTool creates the get-nft tool. +func initNFTTool() api.ServerTool { + props := map[string]*jsonschema.Schema{ + "node": { + Type: "string", + Description: "Name of the node from where packet filtering and classification rules are expected to be extracted", + }, + "namespace": { + Type: "string", + Description: "Namespace of the debug pod from where packet filtering and classification rules are expected to be extracted (optional, defaults to 'default')", + }, + "command": { + Type: "string", + Description: `These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - list ruleset : The ruleset keyword is used to identify the whole set of tables, chains, etc. Print the ruleset in human-readable format. + - list tables : List all chains and rules of the specified table. + - list chains : List all rules of the specified chain. + - list sets : Display the elements in the specified set. + - list maps : Display the elements in the specified map. + - list flowtables: List all flowtables.`, + Enum: []interface{}{"list ruleset", "list tables", "list chains", "list sets", "list maps", "list flowtables"}, + }, + "address_families": { + Type: "string", + Description: `Address families determine the type of packets which are processed. For each address family, the kernel contains so called hooks at specific stages of + the packet processing paths, which invoke nftables if rules for these hooks exist. + - ip IPv4 address family. + - ip6 IPv6 address family. + - inet Internet (IPv4/IPv6) address family. + - arp ARP address family, handling IPv4 ARP packets. + - bridge Bridge address family, handling packets which traverse a bridge device. + - netdev Netdev address family, handling packets on ingress and egress.`, + Enum: []interface{}{"ip", "ip6", "inet", "arp", "bridge", "netdev"}, + }, + } + utils.AddHeadTailProperties(props, fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnkkernelmcp.DefaultMaxOutputLines)) + utils.AddTimeoutSecondsProperty(props) + + return api.ServerTool{ + Tool: api.Tool{ + Name: "get-nft", + Description: "List nftables packet filtering and classification rules on a Kubernetes node. nftables is the modern replacement for iptables. Use this to inspect firewall rules, packet filtering, and network address translation.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{"node", "command"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Kernel NFtables", true), + }, + Handler: nftHandler, + } +} + +func nftHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + node := p.RequiredString("node") + namespace := p.OptionalString("namespace", "") + command := p.RequiredString("command") + addressFamilies := p.OptionalString("address_families", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Create upstream parameters + upstreamParams := types.ListNFTParams{ + CommonParams: types.CommonParams{ + Node: node, + Namespace: namespace, + }, + Command: command, + AddressFamilies: addressFamilies, + } + + if result, err := utils.ValidateHeadTail(&upstreamParams.HeadTailParams, head, tail, applyTailFirst); result != nil { + return result, err + } + if result, err := utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); result != nil { + return result, err + } + + upstreamServer, err := utils.NewKernelMCPServer(params, utils.KernelDebugImage(params)) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.GetNFT( + params.Context, + &mcp.CallToolRequest{}, + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get nftables data: %w", err)), nil + } + + return api.NewToolCallResult(result.Data, nil), nil +} + +// initIPTool creates the get-ip tool. +func initIPTool() api.ServerTool { + props := map[string]*jsonschema.Schema{ + "node": { + Type: "string", + Description: "Name of the node on which ip command is expected to be executed", + }, + "namespace": { + Type: "string", + Description: "Namespace of the debug pod on which ip command is expected to be executed (optional, defaults to 'default')", + }, + "options": { + Type: "string", + Description: `These options helps in providing more details or formattig output data. + -d, -details : Output more detailed information. + -4 : shortcut for -family inet. + -6 : shortcut for -family inet6. + -r, -resolve : use the system's name resolver to print DNS names instead of host addresses. + -n, -netns : switches ip to the specified network namespace NETNS. + -a, -all : executes specified command over all objects, it depends if command supports this option.`, + }, + "command": { + Type: "string", + Description: `These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - address show : protocol (IP or IPv6) address on a device. + - link show : network device. + - neighbour show : manage ARP or NDISC cache entries. + - netns show : manage network namespaces. + - route show : routing table entry. + - rule show : rule in routing policy database. + - vrf show : manage virtual routing and forwarding devices. + - xfrm state list : show Security Association Database. + - xfrm policy list : show Security Policy Database.`, + Enum: []interface{}{"address show", "link show", "neighbour show", "netns show", "route show", "rule show", "vrf show", "xfrm state list", "xfrm policy list"}, + }, + "filter_parameters": { + Type: "string", + Description: `This allows to mention sub command to get more filtered data. Available sub command varies and supportability depends on what is + already supported with 'ip' utility.`, + }, + } + utils.AddHeadTailProperties(props, fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnkkernelmcp.DefaultMaxOutputLines)) + utils.AddTimeoutSecondsProperty(props) + + return api.ServerTool{ + Tool: api.Tool{ + Name: "get-ip", + Description: "Execute ip commands on a Kubernetes node to show routing, network devices, interfaces, and network namespaces. Part of the iproute2 suite for network configuration inspection.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{"node", "command"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Kernel IP Command", true), + }, + Handler: ipHandler, + } +} + +func ipHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + node := p.RequiredString("node") + namespace := p.OptionalString("namespace", "") + command := p.RequiredString("command") + options := p.OptionalString("options", "") + filterParams := p.OptionalString("filter_parameters", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Create upstream parameters + upstreamParams := types.ListIPParams{ + CommonParams: types.CommonParams{ + Node: node, + Namespace: namespace, + }, + Options: options, + Command: command, + FilterParameters: filterParams, + } + + if result, err := utils.ValidateHeadTail(&upstreamParams.HeadTailParams, head, tail, applyTailFirst); result != nil { + return result, err + } + if result, err := utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); result != nil { + return result, err + } + + upstreamServer, err := utils.NewKernelMCPServer(params, utils.KernelDebugImage(params)) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.GetIPCommandOutput( + params.Context, + &mcp.CallToolRequest{}, + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get ip command output: %w", err)), nil + } + + return api.NewToolCallResult(result.Data, nil), nil +} diff --git a/pkg/toolsets/cni-diagnostics/network-tools/tools.go b/pkg/toolsets/cni-diagnostics/network-tools/tools.go new file mode 100644 index 000000000..9a3e5e6b9 --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/network-tools/tools.go @@ -0,0 +1,260 @@ +package network_tools + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/utils" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + ovnknetmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types" + + "k8s.io/utils/ptr" +) + +func InitNetworkTools() []api.ServerTool { + return []api.ServerTool{ + initTcpdumpTool(), + initPwruTool(), + } +} + +// initTcpdumpTool creates the tcpdump tool. +func initTcpdumpTool() api.ServerTool { + return api.ServerTool{ + Tool: api.Tool{ + Name: "tcpdump", + Description: "Capture network packets on a node or inside a pod with BPF filtering. Creates a specialized debug pod for node-level captures. IMPORTANT: Use restrictive BPF filters and low packet counts to avoid performance impact. Maximum 1000 packets.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "target_type": { + Type: "string", + Description: "Capture target: 'node' (node-level) or 'pod' (pod network namespace)", + Enum: []interface{}{"node", "pod"}, + }, + "name": { + Type: "string", + Description: "Name of the target (node or pod)", + }, + "namespace": { + Type: "string", + Description: "Namespace of the target (node or pod). Required when target_type is 'pod'. Optional when target_type is 'node' and defaults to 'default'.", + }, + "container_name": { + Type: "string", + Description: "Name of the container in the pod when target_type is 'pod' (optional, uses default container if not specified)", + }, + "interface": { + Type: "string", + Description: "Network interface name or 'any' (optional, captures on all interfaces if not specified)", + }, + "packet_count": { + Type: "integer", + Description: fmt.Sprintf("Number of packets to capture (default: %d, max: %d)", ovnknetmcp.DefaultPacketCount, ovnknetmcp.MaxPacketCount), + Minimum: ptr.To(float64(0)), + Maximum: ptr.To(float64(ovnknetmcp.MaxPacketCount)), + }, + "bpf_filter": { + Type: "string", + Description: "BPF filter expression (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1')", + }, + "snaplen": { + Type: "integer", + Description: fmt.Sprintf("Snapshot length in bytes (default: %d, max: %d). Use %d for headers only, %d for full packets.", ovnknetmcp.DefaultSnaplen, ovnknetmcp.MaxSnaplen, ovnknetmcp.DefaultSnaplen, ovnknetmcp.MaxSnaplen), + Minimum: ptr.To(float64(0)), + Maximum: ptr.To(float64(ovnknetmcp.MaxSnaplen)), + }, + "timeout_seconds": utils.TimeoutSecondsSchema(), + }, + Required: []string{"target_type", "name"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Network Packet Capture", false), + }, + Handler: tcpdumpHandler, + } +} + +func tcpdumpHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + targetType := p.RequiredString("target_type") + name := p.RequiredString("name") + namespace := p.OptionalString("namespace", "") + containerName := p.OptionalString("container_name", "") + iface := p.OptionalString("interface", "") + packetCount := int(p.OptionalInt64("packet_count", 0)) + bpfFilter := p.OptionalString("bpf_filter", "") + snaplen := int(p.OptionalInt64("snaplen", 0)) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Validate target type + if targetType != "node" && targetType != "pod" { + return utils.ValidationErrorResult("target_type must be 'node' or 'pod'") + } + // Validate name + if name == "" { + return utils.ValidationErrorResult(fmt.Sprintf("name is required when target_type is '%s'", targetType)) + } + // Validate namespace + if targetType == "pod" && namespace == "" { + return utils.ValidationErrorResult("namespace is required when target_type is 'pod'") + } + + // Create upstream parameters + upstreamParams := types.TcpdumpParams{ + BaseNetworkDiagParams: types.BaseNetworkDiagParams{ + BPFFilter: bpfFilter, + }, + TargetType: targetType, + Name: name, + Namespace: namespace, + ContainerName: containerName, + Interface: iface, + } + + // Validate packet count + var validationResult *api.ToolCallResult + var validationErr error + packetCount, validationResult, validationErr = utils.NormalizeBoundedInt("packet_count", packetCount, ovnknetmcp.MaxPacketCount, ovnknetmcp.DefaultPacketCount) + if validationResult != nil { + return validationResult, validationErr + } + upstreamParams.PacketCount = packetCount + + // Validate snaplen + snaplen, validationResult, validationErr = utils.NormalizeBoundedInt("snaplen", snaplen, ovnknetmcp.MaxSnaplen, ovnknetmcp.DefaultSnaplen) + if validationResult != nil { + return validationResult, validationErr + } + upstreamParams.Snaplen = snaplen + + if validationResult, validationErr = utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); validationResult != nil { + return validationResult, validationErr + } + + // Create upstream MCP server with our adapters + upstreamServer, err := utils.NewNetworkToolsMCPServer(params, ovnknetmcp.Config{ + TcpdumpImage: utils.TcpdumpImage(params), + }) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.Tcpdump( + params.Context, + &mcp.CallToolRequest{}, + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to capture packets: %w", err)), nil + } + + return api.NewToolCallResult(utils.FormatCommandOutput(result.Output, result.Stderr), nil), nil +} + +// initPwruTool creates the pwru tool. +func initPwruTool() api.ServerTool { + return api.ServerTool{ + Tool: api.Tool{ + Name: "pwru", + Description: "Trace packets through the Linux kernel networking stack using eBPF. pwru (packet, where are you?) shows which kernel functions process a packet, helping debug packet drops and routing issues. Creates a specialized debug pod with eBPF capabilities.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "node_name": { + Type: "string", + Description: "Name of the node to run pwru on", + }, + "node_pod_namespace": { + Type: "string", + Description: "Namespace of the debug pod on which the command is expected to be executed (optional, defaults to 'default')", + }, + "bpf_filter": { + Type: "string", + Description: "BPF filter expression to match packets (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1')", + }, + "output_limit_lines": { + Type: "integer", + Description: fmt.Sprintf("Maximum number of trace events to capture (default: %d, max: %d)", ovnknetmcp.DefaultOutputLimitLines, ovnknetmcp.MaxOutputLimitLines), + Minimum: ptr.To(float64(0)), + Maximum: ptr.To(float64(ovnknetmcp.MaxOutputLimitLines)), + }, + "timeout_seconds": utils.TimeoutSecondsSchema(), + }, + Required: []string{"node_name"}, + }, + Annotations: utils.ReadOnlyAnnotations("CNI-Diagnostics: Network eBPF Packet Tracing", false), + }, + Handler: pwruHandler, + } +} + +func pwruHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + + // Extract parameters + nodeName := p.RequiredString("node_name") + nodePodNamespace := p.OptionalString("node_pod_namespace", "") + bpfFilter := p.OptionalString("bpf_filter", "") + outputLimitLines := int(p.OptionalInt64("output_limit_lines", 0)) + timeoutSecs := p.OptionalInt64("timeout_seconds", 0) + + if result, err, ok := utils.ParamsErrorResult(p); ok { + return result, err + } + + // Create upstream parameters + upstreamParams := types.PwruParams{ + BaseNetworkDiagParams: types.BaseNetworkDiagParams{ + BPFFilter: bpfFilter, + }, + NodeName: nodeName, + NodePodNamespace: nodePodNamespace, + } + + // Validate output limit lines + var validationResult *api.ToolCallResult + var validationErr error + outputLimitLines, validationResult, validationErr = utils.NormalizeBoundedInt( + "output_limit_lines", + outputLimitLines, + ovnknetmcp.MaxOutputLimitLines, + ovnknetmcp.DefaultOutputLimitLines, + ) + if validationResult != nil { + return validationResult, validationErr + } + upstreamParams.OutputLimitLines = outputLimitLines + + if validationResult, validationErr = utils.ValidateTimeout(&upstreamParams.TimeoutParams, timeoutSecs); validationResult != nil { + return validationResult, validationErr + } + + // Create upstream MCP server with our adapters + upstreamServer, err := utils.NewNetworkToolsMCPServer(params, ovnknetmcp.Config{ + PwruImage: utils.PwruImage(params), + }) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create MCP server: %w", err)), nil + } + + // Call upstream handler + _, result, err := upstreamServer.Pwru( + params.Context, + &mcp.CallToolRequest{}, + upstreamParams, + ) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to trace packets: %w", err)), nil + } + + return api.NewToolCallResult(utils.FormatCommandOutput(result.Output, result.Stderr), nil), nil +} diff --git a/pkg/toolsets/cni-diagnostics/toolset.go b/pkg/toolsets/cni-diagnostics/toolset.go new file mode 100644 index 000000000..757e1a22e --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/toolset.go @@ -0,0 +1,45 @@ +package cnidiagnostics + +import ( + "slices" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/kernel" + network_tools "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/network-tools" +) + +type Toolset struct{} + +var _ api.Toolset = (*Toolset)(nil) + +func (t *Toolset) GetName() string { + return "cni-diagnostics" +} + +func (t *Toolset) GetDescription() string { + return "Tools for Container Network Interface (CNI) diagnostics and troubleshooting" +} + +func (t *Toolset) GetTools(_ api.FilteringProvider) []api.ServerTool { + return slices.Concat( + kernel.InitKernelTools(), + network_tools.InitNetworkTools(), + ) +} + +func (t *Toolset) GetPrompts() []api.ServerPrompt { + return nil +} + +func (t *Toolset) GetResources() []api.ServerResource { + return nil +} + +func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate { + return nil +} + +func init() { + toolsets.Register(&Toolset{}) +} diff --git a/pkg/toolsets/cni-diagnostics/utils/utils.go b/pkg/toolsets/cni-diagnostics/utils/utils.go new file mode 100644 index 000000000..3e8c4be97 --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/utils/utils.go @@ -0,0 +1,175 @@ +package utils + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/adapter" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/cni-diagnostics/config" + "github.com/google/jsonschema-go/jsonschema" + ovnkkernelmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp" + ovnknetmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" + + "k8s.io/utils/ptr" +) + +// ParamsErrorResult returns an invalid-parameters result when p.Err() is non-nil. +// The third return value is true when an error result was produced. +func ParamsErrorResult(p *api.Params) (*api.ToolCallResult, error, bool) { + if err := p.Err(); err != nil { + return api.NewToolCallResult("", fmt.Errorf("invalid parameters: %w", err)), nil, true + } + return nil, nil, false +} + +// ValidationErrorResult builds a tool call result for a validation failure message. +func ValidationErrorResult(message string) (*api.ToolCallResult, error) { + return api.NewToolCallResult("", fmt.Errorf("%s", message)), nil +} + +// ValidateHeadTail populates ht from head, tail, and applyTailFirst. +// Returns a validation error result when head or tail is negative. +func ValidateHeadTail(ht *headtail.HeadTailParams, head, tail int, applyTailFirst bool) (*api.ToolCallResult, error) { + if head >= 0 { + ht.Head = head + } else { + return ValidationErrorResult("head must be greater than or equal to 0") + } + if tail >= 0 { + ht.Tail = tail + } else { + return ValidationErrorResult("tail must be greater than or equal to 0") + } + if applyTailFirst { + ht.ApplyTailFirst = true + } + return nil, nil +} + +// ValidateTimeout sets tp.TimeoutSeconds from timeoutSecs, capped at the upstream maximum. +// Returns a validation error result when timeoutSecs is negative. +func ValidateTimeout(tp *timeout.TimeoutParams, timeoutSecs int64) (*api.ToolCallResult, error) { + if timeoutSecs >= 0 { + tp.TimeoutSeconds = uint32(min(timeoutSecs, int64(timeout.MaxTimeout.Seconds()))) + return nil, nil + } + return ValidationErrorResult("timeout_seconds must be greater than or equal to 0") +} + +// NormalizeBoundedInt clamps value to maxVal, applies defaultWhenZero when value is zero, +// and returns a validation error result when value is negative. +func NormalizeBoundedInt(fieldName string, value, maxVal, defaultWhenZero int) (int, *api.ToolCallResult, error) { + if value >= 0 { + value = min(value, maxVal) + if value == 0 { + value = defaultWhenZero + } + return value, nil, nil + } + result, err := ValidationErrorResult(fmt.Sprintf("%s must be greater than or equal to 0", fieldName)) + return 0, result, err +} + +// KernelDebugImage returns the container image for kernel debug node commands, +// using toolset configuration when present. +func KernelDebugImage(params api.ToolHandlerParams) string { + image := config.DefaultNetshootImage + if cniCfg := config.GetConfig(params); cniCfg != nil { + image = cniCfg.KernelDebugImage + } + return image +} + +// TcpdumpImage returns the container image for tcpdump node commands, +// using toolset configuration when present. +func TcpdumpImage(params api.ToolHandlerParams) string { + image := config.DefaultNetshootImage + if cniCfg := config.GetConfig(params); cniCfg != nil { + image = cniCfg.TcpdumpImage + } + return image +} + +// PwruImage returns the container image for pwru node commands, +// using toolset configuration when present. +func PwruImage(params api.ToolHandlerParams) string { + image := config.DefaultPwruImage + if cniCfg := config.GetConfig(params); cniCfg != nil { + image = cniCfg.PwruImage + } + return image +} + +// NewKernelMCPServer creates an upstream kernel MCP server wired to kubernetes-mcp-server +// node debug execution. +func NewKernelMCPServer(params api.ToolHandlerParams, image string) (*ovnkkernelmcp.MCPServer, error) { + return ovnkkernelmcp.NewMCPServer( + adapter.NewRunDebugNodeCommand(params.KubernetesClient), + ovnkkernelmcp.Config{Image: image}, + ) +} + +// NewNetworkToolsMCPServer creates an upstream network-tools MCP server wired to +// kubernetes-mcp-server node debug and pod exec execution. +func NewNetworkToolsMCPServer(params api.ToolHandlerParams, cfg ovnknetmcp.Config) (*ovnknetmcp.MCPServer, error) { + return ovnknetmcp.NewMCPServer( + adapter.NewRunDebugNodeCommand(params.KubernetesClient), + adapter.NewRunPodExecCommand(params.KubernetesClient), + cfg, + ) +} + +// FormatCommandOutput returns output unchanged when stderr is empty; otherwise it +// labels stdout and stderr in a single string. +func FormatCommandOutput(output, stderr string) string { + if stderr == "" { + return output + } + return fmt.Sprintf("-- stdout --\n%s\n-- stderr --\n%s", output, stderr) +} + +// AddHeadTailProperties adds head, tail, and apply_tail_first JSON schema properties to props. +func AddHeadTailProperties(props map[string]*jsonschema.Schema, headDescription string) { + props["head"] = &jsonschema.Schema{ + Type: "integer", + Description: headDescription, + Minimum: ptr.To(float64(0)), + } + props["tail"] = &jsonschema.Schema{ + Type: "integer", + Description: "Return only last N lines", + Minimum: ptr.To(float64(0)), + } + props["apply_tail_first"] = &jsonschema.Schema{ + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + } +} + +// AddTimeoutSecondsProperty adds the timeout_seconds JSON schema property to props. +func AddTimeoutSecondsProperty(props map[string]*jsonschema.Schema) { + props["timeout_seconds"] = TimeoutSecondsSchema() +} + +// TimeoutSecondsSchema returns the JSON schema for the timeout_seconds tool parameter. +func TimeoutSecondsSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "integer", + Description: fmt.Sprintf("Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds.", uint32(timeout.MaxTimeout.Seconds())), + Minimum: ptr.To(float64(0)), + Maximum: ptr.To(float64(timeout.MaxTimeout.Seconds())), + } +} + +// ReadOnlyAnnotations returns MCP tool annotations for read-only CNI Diagnostics tools. +func ReadOnlyAnnotations(title string, idempotent bool) api.ToolAnnotations { + return api.ToolAnnotations{ + Title: title, + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(idempotent), + OpenWorldHint: ptr.To(true), + } +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/conntrack.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/conntrack.go new file mode 100644 index 000000000..b87419aa7 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/conntrack.go @@ -0,0 +1,122 @@ +package mcp + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +const conntrackSystemFile = "/proc/net/nf_conntrack" + +// GetConntrack MCP handler for conntrack operations. +// GetConntrack retrieves connection tracking entries from a Kubernetes node. +// TODO: Add support for conntrack event monitoring (-E flag). +// TODO: Add support for -G (get specific entry) command. +func (s *MCPServer) GetConntrack(ctx context.Context, req *mcp.CallToolRequest, in types.ListConntrackParams) (*mcp.CallToolResult, types.Result, error) { + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + // Falls back to /proc/net/nf_conntrack parsing when conntrack CLI unavailable. + err := s.utilityExists(ctx, in.Namespace, in.Node, "conntrack") + conntrackCliAvailable := err == nil // true if conntrack CLI is available, false otherwise + if err := validateConntrackCommand(in.Command, conntrackCliAvailable); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of conntrack entries: %w", err) + } + if err := utils.ValidateSafeString(in.FilterParameters, "filter parameters", true, utils.ShellMetaCharactersTypeDefault); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of conntrack entries: %w", err) + } + + var stdout, stderr, summary string + if !conntrackCliAvailable { + stdout, stderr, err = s.getConntrackFromFile(ctx, in.Namespace, in.Node) + } else { + stdout, stderr, err = s.getConntrackUsingCLI(ctx, in.Namespace, in.Node, strings.TrimSpace(in.Command), in.FilterParameters) + } + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of conntrack entries: %w", err) + } + + if stderr != "" { + // Split the stderr into summary and remaining stderr + remainingStderr := "" + summary, remainingStderr = splitConntrackSummary(stderr) + if remainingStderr != "" { + return nil, types.Result{}, fmt.Errorf("error while running command: %s", remainingStderr) + } + } + + // Strip empty lines from the output + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxOutputLines) + // Join the lines back into a single string + stdout = strings.Join(lines, "\n") + // If conntrack summary is present, add it to the output + if summary != "" { + stdout = fmt.Sprintf("%s\n -- conntrack summary --\n%s", stdout, summary) + } + return nil, types.Result{Data: stdout}, nil +} + +// getConntrackUsingCLI executes conntrack CLI commands. +func (s *MCPServer) getConntrackUsingCLI(ctx context.Context, namespace, node, command, filterParameters string) (string, string, error) { + cmd := commandbuilder.NewCommand("conntrack") + switch command { + case "-L", "--dump": + cmd.Add(command) + cmd.Add(strings.Fields(filterParameters)...) + case "-S", "--stats": + cmd.Add(command) + case "-C", "--count": + cmd.Add(command) + default: + cmd.Add("-L") + cmd.Add(strings.Fields(filterParameters)...) + } + return s.executeCommand(ctx, namespace, node, cmd.Build()) +} + +// getConntrackFromFile parses /proc/net/nf_conntrack directly. +// TODO: Add filter support while getting conntrack entries from /proc/net/nf_conntrack. +func (s *MCPServer) getConntrackFromFile(ctx context.Context, namespace, node string) (string, string, error) { + cmd := commandbuilder.NewCommand("cat") + cmd.Add(conntrackSystemFile) + return s.executeCommand(ctx, namespace, node, cmd.Build()) +} + +// validateConntrackCommand validates the command to be used to get list of conntrack entries. +// It returns an error if conntrack CLI is not available and any other operation than list is being performed. +func validateConntrackCommand(command string, cliAvailable bool) error { + if command == "" { + return nil + } + if !cliAvailable && (strings.TrimSpace(command) != "-L" && strings.TrimSpace(command) != "--dump") { + return fmt.Errorf("configured image does not have conntrack utility, only -L/--dump is supported with limited filters") + } + if _, err := strconv.Atoi(command); err == nil { + return fmt.Errorf("invalid command: %s", command) + } + validTables := map[string]bool{ + "-L": true, + "-S": true, + "-C": true, + "--dump": true, + "--stats": true, + "--count": true, + } + + if !validTables[strings.TrimSpace(command)] { + return fmt.Errorf("invalid command: %s", command) + } + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/ip.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/ip.go new file mode 100644 index 000000000..75d7ebfba --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/ip.go @@ -0,0 +1,129 @@ +package mcp + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +// GetIPCommandOutput MCP handler for ip utility operations. +// GetIPCommandOutput executes 'ip' utility commands on a node. +// Requires ip utility in the debug container image. +func (s *MCPServer) GetIPCommandOutput(ctx context.Context, req *mcp.CallToolRequest, in types.ListIPParams) (*mcp.CallToolResult, types.Result, error) { + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + err := s.utilityExists(ctx, in.Namespace, in.Node, "ip") + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting ip data: failed to verify ip utility availability in configured image: %w", err) + } + + if err := validateIPCommand(in.Command); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting ip data: %w", err) + } + if err := utils.ValidateSafeString(in.FilterParameters, "filter parameters", true, utils.ShellMetaCharactersTypeDefault); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting ip data: %w", err) + } + if err := utils.ValidateSafeString(in.Options, "options", true, utils.ShellMetaCharactersTypeDefault); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting ip data: %w", err) + } + + cmd := commandbuilder.NewCommand("ip") + cmd.AddIfNotEmpty(in.Options, strings.Fields(in.Options)...) + cmd.Add(strings.Fields(in.Command)...) + cmd.AddIfNotEmpty(in.FilterParameters, strings.Fields(in.FilterParameters)...) + + stdout, stderr, err := s.executeCommand(ctx, in.Namespace, in.Node, cmd.Build()) + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting ip data: %w", err) + } + + if stderr != "" { + return nil, types.Result{}, fmt.Errorf("error while running command: %s", stderr) + } + + // Strip empty lines from the output + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxOutputLines) + // Join the lines back into a single string + stdout = strings.Join(lines, "\n") + return nil, types.Result{Data: stdout}, nil +} + +// validateIPCommand validates that the IP command is allowed. +func validateIPCommand(ipCommand string) error { + if _, err := strconv.Atoi(ipCommand); err == nil { + return fmt.Errorf("invalid ip command: %s", ipCommand) + } + splitCommand := strings.Fields(strings.TrimSpace(ipCommand)) + if len(splitCommand) < 2 { + return fmt.Errorf("invalid ip command: %s", ipCommand) + } + validIPCommand := map[string]bool{ + "address": true, + "link": true, + "neighbour": true, + "netns": true, + "route": true, + "rule": true, + "vrf": true, + "xfrm state": true, + "xfrm policy": true, + } + + // "ip l s" can be used to set a link up or down. This should be considered as an invalid command. + if strings.HasPrefix(splitCommand[0], "l") && splitCommand[1] == "s" { + return fmt.Errorf("invalid ip command: %s", ipCommand) + } + + var valid bool + for command := range validIPCommand { + commandFields := strings.Fields(command) + // Check if the input command matches the expected command pattern + // For single-word commands like "address", we check: splitCommand[0] matches "address" and splitCommand[1] matches "show" + // For multi-word xfrm commands, we check: splitCommand[0:2] matches ["xfrm", "state"/"policy"] and splitCommand[2] matches "list" + + if len(commandFields) == 1 { + // Single-word command (e.g., "address show", "link show") + if strings.HasPrefix(command, splitCommand[0]) && strings.HasPrefix("show", splitCommand[1]) { + valid = true + break + } + } else { + // Multi-word command (e.g., "xfrm state list", "xfrm policy list") + // Need at least len(commandFields) + 1 words (command words + subcommand) + if len(splitCommand) >= len(commandFields)+1 { + allMatch := true + for i, cmdField := range commandFields { + if !strings.HasPrefix(cmdField, splitCommand[i]) { + allMatch = false + break + } + } + // For xfrm state and xfrm policy, only accept "list" + if allMatch && (command == "xfrm state" || command == "xfrm policy") { + if strings.HasPrefix("list", splitCommand[len(commandFields)]) { + valid = true + break + } + } + } + } + } + if !valid { + return fmt.Errorf("invalid ip command: %s", ipCommand) + } + + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/iptables.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/iptables.go new file mode 100644 index 000000000..de19f8ba6 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/iptables.go @@ -0,0 +1,126 @@ +package mcp + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +// GetIptables MCP handler for iptables operations. +// GetIptables retrieves iptables/ip6tables rules from a Kubernetes node. +// Automatically detects IPv6 and uses ip6tables when needed. +func (s *MCPServer) GetIptables(ctx context.Context, req *mcp.CallToolRequest, in types.ListIPTablesParams) (*mcp.CallToolResult, types.Result, error) { + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + err := s.utilityExists(ctx, in.Namespace, in.Node, "iptables") + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of iptables rules: failed to verify iptables utility availability in configured image: %w", err) + } + + if err := validateTableName(in.Table); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of iptables rules: %w", err) + } + if err := validateIptablesCommand(in.Command); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of iptables rules: %w", err) + } + + if err := utils.ValidateSafeString(in.FilterParameters, "filter parameters", true, utils.ShellMetaCharactersTypeDefault); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of iptables rules: %w", err) + } + + table := strings.TrimSpace(in.Table) + command := strings.TrimSpace(in.Command) + cmd := commandbuilder.NewCommand(iptablesCommand(in.FilterParameters)) + // Defaults to 'filter' table when not specified + cmd.AddIf(table == "", "-t", "filter") + cmd.AddIfNotEmpty(table, "-t", table) + // Defaults to -L (list) when command not specified + cmd.AddIf(command == "", "-L") + cmd.AddIfNotEmpty(command, command) + // FilterParameters are invalid with -S/--list-rules command + cmd.AddIf(in.FilterParameters != "" && command != "-S" && command != "--list-rules", strings.Fields(in.FilterParameters)...) + + stdout, stderr, err := s.executeCommand(ctx, in.Namespace, in.Node, cmd.Build()) + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting list of iptables rules: %w", err) + } + + if stderr != "" { + return nil, types.Result{}, fmt.Errorf("error while running command: %s", stderr) + } + + // Strip empty lines from the output + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxOutputLines) + // Join the lines back into a single string + stdout = strings.Join(lines, "\n") + return nil, types.Result{Data: stdout}, nil +} + +// iptablesCommand determines whether to use iptables or ip6tables. +func iptablesCommand(filterParameters string) string { + for _, item := range strings.Split(filterParameters, " ") { + // Use of ip6tables CLI is required while either --ipv6 or -6 flag is mentioned + if item == "--ipv6" || (strings.Contains(item, "-") && strings.Contains(item, "6")) { + return "ip6tables" + } + } + return "iptables" +} + +// validateTableName validates iptables table name +func validateTableName(table string) error { + if table == "" { + return nil + } + + if _, err := strconv.Atoi(table); err == nil { + return fmt.Errorf("invalid table name: %s", table) + } + validTables := map[string]bool{ + "filter": true, + "nat": true, + "mangle": true, + "raw": true, + "security": true, + } + + if !validTables[strings.TrimSpace(table)] { + return fmt.Errorf("invalid table name: %s", table) + } + return nil +} + +// validateIptablesCommand only allow list operation. +func validateIptablesCommand(command string) error { + if command == "" { + return nil + } + + if _, err := strconv.Atoi(command); err == nil { + return fmt.Errorf("invalid iptables command: %s", command) + } + validCommand := map[string]bool{ + "-L": true, + "-S": true, + "--list": true, + "--list-rules": true, + } + + if !validCommand[strings.TrimSpace(command)] { + return fmt.Errorf("invalid iptables command: %s", command) + } + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/mcp.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/mcp.go new file mode 100644 index 000000000..cc775de6b --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/mcp.go @@ -0,0 +1,206 @@ +package mcp + +import ( + "context" + "fmt" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" +) + +type RunDebugNodeCommandFuncType func(ctx context.Context, namespace string, nodeName string, image string, command []string, hostPath string, mountPath string, timeout time.Duration) (string, string, error) + +// Config contains the configuration for the kernel MCP server. +type Config struct { + // Image is the container image to use for running the commands on the node. + Image string +} + +// MCPServer provides MCP server functionality for kernel operations. +type MCPServer struct { + runDebugNodeCommand RunDebugNodeCommandFuncType + cfg Config +} + +// NewMCPServer creates a new MCP server instance +func NewMCPServer(runDebugNodeCommand RunDebugNodeCommandFuncType, cfg Config) (*MCPServer, error) { + if runDebugNodeCommand == nil { + return nil, fmt.Errorf("function to run debug node command is nil") + } + return &MCPServer{ + runDebugNodeCommand: runDebugNodeCommand, + cfg: cfg, + }, nil +} + +// AddTools registers all kernel-related MCP tools +func (s *MCPServer) AddTools(server *mcp.Server) { + // get-conntrack tool registration + mcp.AddTool(server, + &mcp.Tool{ + Name: "get-conntrack", + Description: fmt.Sprintf(`get-conntrack allows to interact with the connection tracking system of a Kubernetes node. + Use this command to discover a list of all (or a filtered selection of) currently tracked connections. +Parameters: +- node (required): Name of the node from where conntrack entries are expected to be extracted +- namespace (optional): Namespace of the debug pod from where conntrack entries are expected to be extracted. Default: 'default' +- command (optional): These options specify the particular operation to perform. These options can only be used if configured image has 'conntrack' utility available. If omitted or empty, defaults to -L. + -L, --dump : List connection tracking table. + -C, --count: Show the table counter. + -S, --stats: Show the in-kernel connection tracking system statistics. +- filter_parameters (optional): These parameters are useful to filter certain entries from the whole table: + -s, --src, --orig-src IP_ADDRESS : Match only entries whose source address in the original direction equals to mentioned IP. + -d, --dst, --orig-dst IP_ADDRESS : Match only entries whose destination address in the original direction equals to mentioned IP. + -p, --proto PROTO : Specify layer four (TCP, UDP, ...) protocol. + --sport, --orig-port-src PORT : Source port in original direction. + --dport, --orig-port-dst PORT : Destination port in original direction. +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Example: +- node='ovn-control-plane', command='-L' +- node='ovn-worker', namespace='ovn-kubernetes', command='-L' +- node='ovn-worker', filter_parameters='-s 1.2.3.4 -d 5.6.7.8 -p tcp --sport 32000 --dport 10250' + +Example output: +tcp 6 91 ESTABLISHED src=1.2.3.4 dst=5.6.7.8 sport=32000 dport=10250 src=5.6.7.8 dst=1.2.3.4 sport=10250 dport=32000 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=2 +`, DefaultMaxOutputLines, int(timeout.MaxTimeout.Seconds())), + }, s.GetConntrack) + // get-iptables tool registration + mcp.AddTool(server, + &mcp.Tool{ + Name: "get-iptables", + Description: fmt.Sprintf(`get-iptables allows to interact with kernel to list packet filter rules. + Iptables and ip6tables are used to inspect the tables of IPv4 and IPv6 packet filter rules in the Linux kernel. +Parameters: +- node (required): Name of the node from where packet filter rules are expected to be extracted +- namespace (optional): Namespace of the debug pod from where packet filter rules are expected to be extracted. Default: 'default' +- table (optional): There are currently five independent tables (which tables are present at any time depends on the kernel configuration options and which modules are present). + filter : This is the default table + nat : This table is consulted when a packet that creates a new connection is encountered. + mangle : This table is used for specialized packet alteration. + raw : This table is used mainly for configuring exemptions from connection tracking in combination with the NOTRACK target. + security: This table is used for Mandatory Access Control (MAC) networking rules. +- command (optional): These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. If omitted or empty, defaults to -L. + -L, --list [chain] : List all rules in the selected chain. If no chain is selected, all chains are listed. + -S, --list-rules [chain] : Print all rules in the selected chain. If no chain is selected, all chains are printed like iptables-save. +- filter_parameters (optional): These parameters are useful to filter certain entries from the whole table: + -s, --source address[/mask] : Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. + -d, --destination address[/mask] : Destination specification. + -v, --verbose : Verbose output. + -n, --numeric : Numeric output. IP addresses and port numbers will be printed in numeric format. + -p, --protocol protocol : The protocol of the rule or of the packet to check. + -4, --ipv4 : IPv4 + -6, --ipv6 : IPv6 +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Example: +- node='ovn-control-plane', table='nat', filter_parameters='-nv4' +- node='ovn-control-plane', table='nat', command='-L' +Example output: +Chain POSTROUTING (policy ACCEPT 675K packets, 41M bytes) + pkts bytes target prot opt in out source destination + 675K 41M OVN-KUBE-EGRESS-IP-MULTI-NIC all -- * * 0.0.0.0/0 0.0.0.0/0 +`, DefaultMaxOutputLines, int(timeout.MaxTimeout.Seconds())), + }, s.GetIptables) + // get-nft tool registration + mcp.AddTool(server, + &mcp.Tool{ + Name: "get-nft", + Description: fmt.Sprintf(`get-nft allows to interact with kernel to list packet filtering and classification rules. +Parameters: +- node (required): Name of the node from where packet filtering and classification rules are expected to be extracted +- namespace (optional): Namespace of the debug pod from where packet filtering and classification rules are expected to be extracted. Default: 'default' +- command (required): These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - list ruleset : The ruleset keyword is used to identify the whole set of tables, chains, etc. Print the ruleset in human-readable format. + - list tables : List all chains and rules of the specified table. + - list chains : List all rules of the specified chain. + - list sets : Display the elements in the specified set. + - list maps : Display the elements in the specified map. + - list flowtables: List all flowtables. +- address_families (optional): Address families determine the type of packets which are processed. For each address family, the kernel contains so-called hooks at specific stages of + the packet processing paths, which invoke nftables if rules for these hooks exist. + - ip IPv4 address family. + - ip6 IPv6 address family. + - inet Internet (IPv4/IPv6) address family. + - arp ARP address family, handling IPv4 ARP packets. + - bridge Bridge address family, handling packets which traverse a bridge device. + - netdev Netdev address family, handling packets on ingress and egress. +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Example: +- node='ovn-control-plane', command='list tables', address_families='inet' +Example output: +table inet ovn-kubernetes +`, DefaultMaxOutputLines, int(timeout.MaxTimeout.Seconds())), + }, s.GetNFT) + // get-ip tool registration + mcp.AddTool(server, + &mcp.Tool{ + Name: "get-ip", + Description: fmt.Sprintf(`get-ip allows to interact with kernel to list routing, network devices, interfaces. +Parameters: +- node (required): Name of the node on which ip command is expected to be executed +- namespace (optional): Namespace of the debug pod on which ip command is expected to be executed. Default: 'default' +- options (optional): These options helps in providing more details or formatting output data. + -d, -details : Output more detailed information. + -4 : shortcut for -family inet. + -6 : shortcut for -family inet6. + -r, -resolve : use the system's name resolver to print DNS names instead of host addresses. + -n, -netns : switches ip to the specified network namespace NETNS. + -a, -all : executes specified command over all objects, it depends if command supports this option. +- command (required): These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - address show : protocol (IP or IPv6) address on a device. + - link show : network device. + - neighbour show : manage ARP or NDISC cache entries. + - netns show : manage network namespaces. + - route show : routing table entry. + - rule show : rule in routing policy database. + - vrf show : manage virtual routing and forwarding devices. + - xfrm state list : show Security Association Database. + - xfrm policy list : show Security Policy Database. +- filter_parameters (optional): This allows to mention sub command to get more filtered data. Available sub command varies and supportability depends on what is + already supported with 'ip' utility. +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Example: +- node='ovn-control-plane', options="-4", command='route show', filter_parameters='table all' +Example output: +default via 10.0.0.254 dev br-ex proto dhcp src 10.0.0.10 metric 48 +`, DefaultMaxOutputLines, int(timeout.MaxTimeout.Seconds())), + }, s.GetIPCommandOutput) +} + +// executeCommand executes a command on a node via kubectl debug +func (s *MCPServer) executeCommand(ctx context.Context, namespace, node string, command []string) (string, string, error) { + stdout, stderr, err := s.runDebugNodeCommand(ctx, namespace, node, s.cfg.Image, command, "", "", 0) + if err != nil { + return "", "", fmt.Errorf("error while establishing tty connection to the node: %w", err) + } + + // Filter out warning lines from stderr + if stderr != "" { + stderr = filterWarnings(stderr) + } + + // Filter out warning lines from stdout + stdout = filterWarnings(stdout) + + return stdout, stderr, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/nft.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/nft.go new file mode 100644 index 000000000..b8ad13a78 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/nft.go @@ -0,0 +1,106 @@ +package mcp + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +// GetNFT MCP handler for nftables operations. +// GetNFT retrieves nftables configuration from a Kubernetes node. +func (s *MCPServer) GetNFT(ctx context.Context, req *mcp.CallToolRequest, in types.ListNFTParams) (*mcp.CallToolResult, types.Result, error) { + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + err := s.utilityExists(ctx, in.Namespace, in.Node, "nft") + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting nft data: failed to verify nft utility availability in configured image: %w", err) + } + + if err := validateNFTCommand(in.Command); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting nft data: %w", err) + } + if err := utils.ValidateSafeString(in.AddressFamilies, "address families", true, utils.ShellMetaCharactersTypeDefault); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting nft data: %w", err) + } + if err := validateNFTAddressFamily(in.AddressFamilies); err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting nft data: %w", err) + } + + command := strings.TrimSpace(in.Command) + addressFamilies := strings.TrimSpace(in.AddressFamilies) + cmd := commandbuilder.NewCommand("nft") + cmd.Add(strings.Fields(command)...) + cmd.AddIf(addressFamilies != "", addressFamilies) + + stdout, stderr, err := s.executeCommand(ctx, in.Namespace, in.Node, cmd.Build()) + if err != nil { + return nil, types.Result{}, fmt.Errorf("error while getting nft data: %w", err) + } + + if stderr != "" { + return nil, types.Result{}, fmt.Errorf("error while running command: %s", stderr) + } + + // Strip empty lines from the output + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxOutputLines) + // Join the lines back into a single string + stdout = strings.Join(lines, "\n") + return nil, types.Result{Data: stdout}, nil +} + +// validateNFTCommand validates nftables command. This is to put a limitation on the use of the tool. +func validateNFTCommand(command string) error { + if _, err := strconv.Atoi(command); err == nil { + return fmt.Errorf("invalid nft command: %s", command) + } + validCommand := map[string]bool{ + "list ruleset": true, + "list tables": true, + "list chains": true, + "list sets": true, + "list maps": true, + "list flowtables": true, + } + + if !validCommand[strings.TrimSpace(command)] { + return fmt.Errorf("invalid nft command: %s", command) + } + return nil +} + +// validateNFTAddressFamily validates nftables address family. +func validateNFTAddressFamily(addrFamily string) error { + if addrFamily == "" { + return nil + } + + if _, err := strconv.Atoi(addrFamily); err == nil { + return fmt.Errorf("invalid nft address family: %s", addrFamily) + } + validaddrFamily := map[string]bool{ + "ip": true, + "ip6": true, + "inet": true, + "arp": true, + "bridge": true, + "netdev": true, + } + + if !validaddrFamily[strings.TrimSpace(addrFamily)] { + return fmt.Errorf("invalid nft address family: %s", addrFamily) + } + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/util.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/util.go new file mode 100644 index 000000000..2fd7c1b6a --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp/util.go @@ -0,0 +1,74 @@ +package mcp + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +const ( + // DefaultMaxOutputLines defines the maximum number of lines to return from command output + DefaultMaxOutputLines = 100 +) + +// ConntrackSummaryPattern matches conntrack informational stderr printed on successful -L/--dump. +// Format: "conntrack v (conntrack-tools): flow entries have been shown." +var ConntrackSummaryPattern = regexp.MustCompile(`^conntrack v\d+\.\d+\.\d+ \(conntrack-tools\): \d+ flow entries have been shown\.?$`) + +// utilityExists checks if a utility/command exists in the container +func (s *MCPServer) utilityExists(ctx context.Context, namespace, node, utility string) error { + cmd := commandbuilder.NewCommand(utility, "-V") + _, stderr, err := s.runDebugNodeCommand(ctx, namespace, node, s.cfg.Image, cmd.Build(), "", "", 0) + if err != nil { + return fmt.Errorf("error while checking availability of the utility %s: %w", utility, err) + } + stderr = strings.TrimSpace(filterWarnings(stderr)) + if stderr != "" { + return fmt.Errorf("utility %s is unavailable in configured image: %s", utility, stderr) + } + return nil +} + +// filterWarnings filters out lines starting with "Warning" from the output +func filterWarnings(output string) string { + if output == "" { + return output + } + + lines := strings.Split(output, "\n") + var filteredLines []string + + for _, line := range lines { + if !strings.Contains(line, "Warning") && !strings.Contains(line, "warning") && !strings.Contains(line, "WARNING") { + filteredLines = append(filteredLines, line) + } + } + + return strings.Join(filteredLines, "\n") +} + +// splitConntrackSummary separates conntrack informational stderr from other stderr. +// The conntrack informational stderr is printed when using conntrack CLI -L/--dump command. +// Format: "conntrack v (conntrack-tools): flow entries have been shown." +func splitConntrackSummary(output string) (summary, remaining string) { + if output == "" { + return "", "" + } + + lines := strings.Split(output, "\n") + var summaryLines []string + var remainingLines []string + + for _, line := range lines { + if ConntrackSummaryPattern.MatchString(line) { + summaryLines = append(summaryLines, line) + } else { + remainingLines = append(remainingLines, line) + } + } + + return strings.Join(summaryLines, "\n"), strings.Join(remainingLines, "\n") +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types/types.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types/types.go new file mode 100644 index 000000000..f0eceea54 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types/types.go @@ -0,0 +1,55 @@ +package types + +import ( + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" +) + +// CommonParams contains the common parameters required for executing kernel commands on a Kubernetes node. +// These parameters are embedded in other specific parameter types. +type CommonParams struct { + Node string `json:"node"` // Node is the name of the Kubernetes node where the command will be executed + Namespace string `json:"namespace,omitempty"` // Namespace is the namespace of the Kubernetes node where the command will be executed + headtail.HeadTailParams + timeout.TimeoutParams +} + +// ListConntrackParams contains parameters for listing connection tracking entries using conntrack command. +// Connection tracking entries show active network connections and their state. +type ListConntrackParams struct { + CommonParams + Command string `json:"command,omitempty"` // Command specifies the conntrack command to execute (e.g., "list", "dump") + FilterParameters string `json:"filter_parameters,omitempty"` // FilterParameters specifies additional filter criteria for conntrack entries +} + +// ListIPTablesParams contains parameters for inspecting iptables/ip6tables packet filter rules. +// Supports both IPv4 (iptables) and IPv6 (ip6tables) firewall rules. +type ListIPTablesParams struct { + CommonParams + Table string `json:"table,omitempty"` // Table specifies the iptables table to query (e.g., "filter", "nat", "mangle", "raw") + Command string `json:"command,omitempty"` // Command specifies the iptables action (e.g., "-L", "-S"). If omitted or empty, defaults to "-L" + FilterParameters string `json:"filter_parameters,omitempty"` // FilterParameters specifies additional filter criteria for iptables rules +} + +// ListNFTParams contains parameters for inspecting nftables packet filtering and classification rules. +// nftables is the modern replacement for iptables in the Linux kernel. +type ListNFTParams struct { + CommonParams + Command string `json:"command"` // Command specifies the nft command to execute + AddressFamilies string `json:"address_families,omitempty"` // AddressFamilies specifies the address family to filter (e.g., "ip", "ip6", "inet", "arp", "bridge") +} + +// ListIPParams contains parameters for inspecting routing, network devices, and network interfaces. +// Uses the iproute2 suite (ip command) for network configuration and inspection. +type ListIPParams struct { + CommonParams + Options string `json:"options,omitempty"` // Options specifies additional command-line options for the ip command + Command string `json:"command"` // Command specifies the ip subcommand to execute (e.g., "route", "link", "addr", "neigh") + FilterParameters string `json:"filter_parameters,omitempty"` // FilterParameters specifies additional filter criteria for the output +} + +// Result represents the output returned from executing a kernel command. +// The data contains the command's stdout/stderr output. +type Result struct { + Data string `json:"data"` // Data contains the command execution output +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/mcp.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/mcp.go new file mode 100644 index 000000000..1b547d689 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/mcp.go @@ -0,0 +1,97 @@ +package mcp + +import ( + "context" + "fmt" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" +) + +type RunDebugNodeCommandFuncType func(ctx context.Context, namespace string, nodeName string, image string, command []string, hostPath string, mountPath string, timeout time.Duration) (string, string, error) +type RunPodExecCommandFuncType func(ctx context.Context, namespace, name, container string, command []string) (string, string, error) + +// Config contains the configuration for the network tools MCP server. +type Config struct { + // PwruImage is the container image to use for running the pwru command on the node. + PwruImage string + // TcpdumpImage is the container image to use for running the tcpdump command on the node. + TcpdumpImage string +} + +// MCPServer provides MCP server functionality for network tools operations. +type MCPServer struct { + runDebugNodeCommand RunDebugNodeCommandFuncType + runPodExecCommand RunPodExecCommandFuncType + cfg Config +} + +// NewMCPServer creates a new MCP server instance +func NewMCPServer(runNodeCommand RunDebugNodeCommandFuncType, runPodExecCommand RunPodExecCommandFuncType, cfg Config) (*MCPServer, error) { + if runNodeCommand == nil { + return nil, fmt.Errorf("function to run debug node command is nil") + } + if runPodExecCommand == nil { + return nil, fmt.Errorf("function to run pod exec command is nil") + } + return &MCPServer{ + runDebugNodeCommand: runNodeCommand, + runPodExecCommand: runPodExecCommand, + cfg: cfg, + }, nil +} + +// AddTools registers network tools with the MCP server +func (s *MCPServer) AddTools(server *mcp.Server) { + mcp.AddTool(server, + &mcp.Tool{ + Name: "tcpdump", + Description: fmt.Sprintf(`Capture network packets on a node or inside a pod with strict safety controls. + +Supports both node-level and pod-level packet capture with BPF filtering. + +This tool creates a specialized debug pod on the specified node for node-level captures. + +Parameters: +- target_type: 'node' or 'pod' (required) +- name: Name of the target (node or pod) (required) +- namespace: Namespace of the target (node or pod). Required when target_type is 'pod'. Optional when target_type is 'node' and defaults to 'default'. +- container_name: Name of the container in the pod when target_type is 'pod' (optional, uses default container if not specified) +- interface: Network interface name or 'any' (optional, uses default if not specified) +- packet_count: Number of packets to capture (default: 100, max: 1000) +- bpf_filter: BPF filter expression to match packets (optional, e.g., "tcp and dst port 8080", "host 10.0.0.1") +- snaplen: Snapshot length in bytes (default: 96, max: 1500) +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Examples: +- Capture on node: {"target_type": "node", "name": "worker-1", "interface": "eth0", "packet_count": 100, "bpf_filter": "tcp port 80"} +- Capture in pod: {"target_type": "pod", "name": "my-pod", "namespace": "default", "interface": "eth0", "packet_count": 100, "bpf_filter": "host 10.0.0.1"} +- Capture DNS: {"target_type": "node", "name": "worker-1", "interface": "any", "packet_count": 50, "bpf_filter": "port 53"}`, + int(timeout.MaxTimeout.Seconds())), + }, s.Tcpdump) + mcp.AddTool(server, + &mcp.Tool{ + Name: "pwru", + Description: fmt.Sprintf(`Trace packets through the Linux kernel networking stack using eBPF. + +pwru (packet, where are you?) shows which kernel functions process a packet, helping debug packet +drops, routing issues, and understanding the kernel's packet processing path. + +This tool creates a specialized debug pod on the specified node with necessary eBPF capabilities +to trace packets through kernel networking functions. + +Parameters: +- node_name: Name of the node to run pwru on (required) +- node_pod_namespace (optional): Namespace of the debug pod on which the command is expected to be executed. Default: 'default' +- bpf_filter: BPF filter expression to match packets (optional, e.g., "tcp and dst port 8080", "host 10.0.0.1") +- output_limit_lines: Maximum number of trace events to capture (default: 100, max: 1000) +- timeout_seconds (optional): Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is %d seconds. + +Examples: +- Basic trace: {"node_name": "worker-1", "bpf_filter": "host 10.244.0.5", "output_limit_lines": 100} +- TCP traffic: {"node_name": "worker-1", "bpf_filter": "tcp and dst port 8080", "output_limit_lines": 50} +- ICMP packets: {"node_name": "worker-1", "bpf_filter": "icmp", "output_limit_lines": 100}`, + int(timeout.MaxTimeout.Seconds())), + }, s.Pwru) +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/pwru.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/pwru.go new file mode 100644 index 000000000..645fdd313 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/pwru.go @@ -0,0 +1,49 @@ +package mcp + +import ( + "context" + "strconv" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +const ( + DefaultOutputLimitLines = 100 + MaxOutputLimitLines = 1000 +) + +// Pwru executes the pwru (packet, where are you?) tool to trace packets through the Linux kernel. +// It creates a specialized debug pod with eBPF capabilities and traces packet processing paths. +// This is useful for debugging packet drops, routing issues, and understanding kernel networking behavior. +func (s *MCPServer) Pwru(ctx context.Context, req *mcp.CallToolRequest, in types.PwruParams) (*mcp.CallToolResult, types.CommandResult, error) { + outputLimitLines := in.OutputLimitLines + if outputLimitLines == 0 { + outputLimitLines = DefaultOutputLimitLines + } + if err := validateIntMax(outputLimitLines, MaxOutputLimitLines, "output_limit_lines", ""); err != nil { + return nil, types.CommandResult{}, err + } + + if err := validatePacketFilter(in.BPFFilter); err != nil { + return nil, types.CommandResult{}, err + } + + cmd := commandbuilder.NewCommand("pwru", "--output-limit-lines", strconv.Itoa(outputLimitLines)) + // pwru accepts pcap filter as positional argument(s) + cmd.AddIfNotEmpty(in.BPFFilter, in.BPFFilter) + + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + stdout, stderr, err := s.runDebugNodeCommand(ctx, in.NodePodNamespace, in.NodeName, s.cfg.PwruImage, cmd.Build(), "/sys/kernel/debug", "/sys/kernel/debug", 0) + if err != nil { + return nil, types.CommandResult{}, err + } + return nil, types.CommandResult{Output: stdout, Stderr: stderr}, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/tcpdump.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/tcpdump.go new file mode 100644 index 000000000..ecec2776a --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/tcpdump.go @@ -0,0 +1,87 @@ +package mcp + +import ( + "context" + "fmt" + "strconv" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder" +) + +const ( + DefaultPacketCount = 100 + MaxPacketCount = 1000 + // DefaultSnaplen is set to 96 bytes to capture headers (Ethernet + IP + TCP/UDP + some payload) + // This is sufficient for most diagnostic purposes while minimizing data capture + DefaultSnaplen = 96 + // MaxSnaplen is set to 1500 bytes (standard Ethernet MTU) to allow full packet capture + // when needed for deeper analysis. Users can set snaplen parameter to capture complete packets. + MaxSnaplen = 1500 +) + +// Tcpdump executes the tcpdump packet capture tool on a node or inside a pod. +func (s *MCPServer) Tcpdump(ctx context.Context, req *mcp.CallToolRequest, in types.TcpdumpParams) (*mcp.CallToolResult, types.CommandResult, error) { + if in.TargetType == "" || (in.TargetType != "node" && in.TargetType != "pod") { + return nil, types.CommandResult{}, fmt.Errorf("target_type is required and must be 'node' or 'pod'") + } + if in.Name == "" { + return nil, types.CommandResult{}, fmt.Errorf("name is required when target_type is '%s'", in.TargetType) + } + if in.TargetType == "pod" && in.Namespace == "" { + return nil, types.CommandResult{}, fmt.Errorf("namespace is required when target_type is 'pod'") + } + if err := validateInterface(in.Interface); err != nil { + return nil, types.CommandResult{}, err + } + if err := validatePacketFilter(in.BPFFilter); err != nil { + return nil, types.CommandResult{}, err + } + + packetCount := in.PacketCount + if packetCount == 0 { + packetCount = DefaultPacketCount + } + if err := validateIntMax(packetCount, MaxPacketCount, "packet_count", ""); err != nil { + return nil, types.CommandResult{}, err + } + + snaplen := in.Snaplen + if snaplen == 0 { + snaplen = DefaultSnaplen + } + if err := validateIntMax(snaplen, MaxSnaplen, "snaplen", "bytes"); err != nil { + return nil, types.CommandResult{}, err + } + + cmd := commandbuilder.NewCommand("tcpdump", "-n", "-v", + "-s", strconv.Itoa(snaplen), + "-c", strconv.Itoa(packetCount)) + cmd.AddIfNotEmpty(in.Interface, "-i", in.Interface) + cmd.AddIfNotEmpty(in.BPFFilter, in.BPFFilter) + + // If timeout is specified, create a new context with timeout + var cancel context.CancelFunc + ctx, cancel = in.TimeoutParams.WithTimeout(ctx) + if cancel != nil { + defer cancel() + } + + switch in.TargetType { + case "node": + stdout, stderr, err := s.runDebugNodeCommand(ctx, in.Namespace, in.Name, s.cfg.TcpdumpImage, cmd.Build(), "", "", 0) + if err != nil { + return nil, types.CommandResult{}, err + } + return nil, types.CommandResult{Output: stdout, Stderr: stderr}, nil + case "pod": + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, in.ContainerName, cmd.Build()) + if err != nil { + return nil, types.CommandResult{}, err + } + return nil, types.CommandResult{Output: stdout, Stderr: stderr}, nil + default: + return nil, types.CommandResult{}, fmt.Errorf("invalid target_type: %s (must be 'node' or 'pod')", in.TargetType) + } +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/utils.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/utils.go new file mode 100644 index 000000000..d886e9184 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp/utils.go @@ -0,0 +1,49 @@ +package mcp + +import ( + "fmt" + "regexp" + + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" +) + +var interfaceNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) + +// validateInterface validates a network interface name for security and correctness. +func validateInterface(iface string) error { + if iface == "" { + return nil + } + if iface == "any" { + return nil + } + if len(iface) > 15 { + return fmt.Errorf("interface name too long: %s", iface) + } + if !interfaceNamePattern.MatchString(iface) { + return fmt.Errorf("invalid interface name: %s", iface) + } + return nil +} + +// validatePacketFilter validates a packet filter expression for security. +func validatePacketFilter(filter string) error { + if len(filter) > 1024 { + return fmt.Errorf("packet filter too long (max 1024 characters)") + } + return utils.ValidateSafeString(filter, "packet filter", true, utils.ShellMetaCharactersTypeDisallowSpecialCharacters) +} + +// validateIntMax checks if a value exceeds the maximum or is negative and returns an error if it does +func validateIntMax(value, max int, fieldName, unit string) error { + if value < 0 { + return fmt.Errorf("%s cannot be negative", fieldName) + } + if value > max { + if unit != "" { + return fmt.Errorf("%s cannot exceed %d %s", fieldName, max, unit) + } + return fmt.Errorf("%s cannot exceed %d", fieldName, max) + } + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types/manifest.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types/manifest.go new file mode 100644 index 000000000..33cd16162 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types/manifest.go @@ -0,0 +1,47 @@ +package types + +import "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" + +// BaseNetworkDiagParams contains common parameters shared across network diagnostic tools. +type BaseNetworkDiagParams struct { + BPFFilter string `json:"bpf_filter,omitempty"` +} + +// TcpdumpParams contains parameters for running tcpdump packet capture. +// Supports both node-level and pod-level packet capture with BPF filtering. +type TcpdumpParams struct { + BaseNetworkDiagParams + + TargetType string `json:"target_type"` + + // Combined node and pod fields + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + + // Pod-specific field + ContainerName string `json:"container_name,omitempty"` + + Interface string `json:"interface,omitempty"` + PacketCount int `json:"packet_count,omitempty"` + Snaplen int `json:"snaplen,omitempty"` + + timeout.TimeoutParams +} + +// PwruParams contains parameters for running pwru (packet, where are you?) eBPF-based +// kernel packet tracing. This tool traces packets through the Linux kernel networking stack. +type PwruParams struct { + BaseNetworkDiagParams + + NodeName string `json:"node_name"` + NodePodNamespace string `json:"node_pod_namespace,omitempty"` + OutputLimitLines int `json:"output_limit_lines,omitempty"` + + timeout.TimeoutParams +} + +// CommandResult represents the output and status of an executed command. +type CommandResult struct { + Output string `json:"output"` + Stderr string `json:"stderr,omitempty"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder/command_builder.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder/command_builder.go new file mode 100644 index 000000000..1128aac3c --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder/command_builder.go @@ -0,0 +1,40 @@ +package commandbuilder + +import "slices" + +// CommandBuilder helps build commands with a fluent interface +type CommandBuilder struct { + args []string +} + +// NewCommand creates a new command builder with the base command +func NewCommand(baseCmd ...string) *CommandBuilder { + return &CommandBuilder{args: slices.Clone(baseCmd)} +} + +// Add adds arguments to the command +func (cb *CommandBuilder) Add(args ...string) *CommandBuilder { + cb.args = append(cb.args, args...) + return cb +} + +// AddIf adds arguments to the command only if the condition is true +func (cb *CommandBuilder) AddIf(condition bool, args ...string) *CommandBuilder { + if condition { + cb.args = append(cb.args, args...) + } + return cb +} + +// AddIfNotEmpty adds arguments to the command only if the value is not empty +func (cb *CommandBuilder) AddIfNotEmpty(value string, args ...string) *CommandBuilder { + if value != "" { + cb.args = append(cb.args, args...) + } + return cb +} + +// Build returns the final command slice +func (cb *CommandBuilder) Build() []string { + return slices.Clone(cb.args) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b9fc2ac0e..e0faa071c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -390,12 +390,17 @@ github.com/opencontainers/image-spec/specs-go/v1 github.com/os-observability/redhat-opentelemetry-collector/configschemas # github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.1.0 ## explicit; go 1.26.0 +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/types github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovs/mcp github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovs/types github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/commandbuilder github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern From 9d50096a4d8439a1f9351ff7bf2f15c19d95c41c Mon Sep 17 00:00:00 2001 From: arkadeepsen Date: Thu, 23 Jul 2026 22:55:11 +0530 Subject: [PATCH 3/5] feat(cni-diagnostics): Add unit tests for kernel and network-tools of cni-diagnostics Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: arkadeepsen Co-authored-by: Cursor --- .../cni-diagnostics/adapter/adapter_test.go | 29 + .../cni-diagnostics/config/config_test.go | 101 +++ .../cni-diagnostics/kernel/tools_test.go | 606 ++++++++++++++++++ .../network-tools/tools_test.go | 454 +++++++++++++ 4 files changed, 1190 insertions(+) create mode 100644 pkg/toolsets/cni-diagnostics/adapter/adapter_test.go create mode 100644 pkg/toolsets/cni-diagnostics/config/config_test.go create mode 100644 pkg/toolsets/cni-diagnostics/kernel/tools_test.go create mode 100644 pkg/toolsets/cni-diagnostics/network-tools/tools_test.go diff --git a/pkg/toolsets/cni-diagnostics/adapter/adapter_test.go b/pkg/toolsets/cni-diagnostics/adapter/adapter_test.go new file mode 100644 index 000000000..6c8e6b441 --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/adapter/adapter_test.go @@ -0,0 +1,29 @@ +package adapter + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type AdapterSuite struct { + suite.Suite +} + +func (s *AdapterSuite) TestNewRunDebugNodeCommand() { + s.Run("returns function with correct signature", func() { + fn := NewRunDebugNodeCommand(nil) + s.NotNil(fn, "adapter should return a non-nil function") + }) +} + +func (s *AdapterSuite) TestNewRunPodExecCommand() { + s.Run("returns function with correct signature", func() { + fn := NewRunPodExecCommand(nil) + s.NotNil(fn, "adapter should return a non-nil function") + }) +} + +func TestAdapter(t *testing.T) { + suite.Run(t, new(AdapterSuite)) +} diff --git a/pkg/toolsets/cni-diagnostics/config/config_test.go b/pkg/toolsets/cni-diagnostics/config/config_test.go new file mode 100644 index 000000000..f3d6ee58a --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/config/config_test.go @@ -0,0 +1,101 @@ +package config + +import ( + "testing" + + "github.com/BurntSushi/toml" + "github.com/stretchr/testify/suite" +) + +type ConfigSuite struct { + suite.Suite +} + +func (s *ConfigSuite) TestConfigDefaults() { + s.Run("empty config has empty fields", func() { + cfg := Config{} + s.Equal("", cfg.KernelDebugImage) + s.Equal("", cfg.TcpdumpImage) + s.Equal("", cfg.PwruImage) + }) +} + +func (s *ConfigSuite) TestConfigParsing() { + s.Run("parses kernel_debug_image", func() { + tomlStr := `kernel_debug_image = "registry.example.com/toolbox:1.0"` + var cfg Config + err := toml.Unmarshal([]byte(tomlStr), &cfg) + s.NoError(err) + s.Equal("registry.example.com/toolbox:1.0", cfg.KernelDebugImage) + }) + + s.Run("parses tcpdump_image", func() { + tomlStr := `tcpdump_image = "nicolaka/netshoot:v0.14"` + var cfg Config + err := toml.Unmarshal([]byte(tomlStr), &cfg) + s.NoError(err) + s.Equal("nicolaka/netshoot:v0.14", cfg.TcpdumpImage) + }) + + s.Run("parses pwru_image", func() { + tomlStr := `pwru_image = "cilium/pwru:v1.1.0"` + var cfg Config + err := toml.Unmarshal([]byte(tomlStr), &cfg) + s.NoError(err) + s.Equal("cilium/pwru:v1.1.0", cfg.PwruImage) + }) + + s.Run("parses all images together", func() { + tomlStr := ` +kernel_debug_image = "registry.example.com/toolbox:1.0" +tcpdump_image = "nicolaka/netshoot:v0.14" +pwru_image = "cilium/pwru:v1.1.0" +` + var cfg Config + err := toml.Unmarshal([]byte(tomlStr), &cfg) + s.NoError(err) + s.Equal("registry.example.com/toolbox:1.0", cfg.KernelDebugImage) + s.Equal("nicolaka/netshoot:v0.14", cfg.TcpdumpImage) + s.Equal("cilium/pwru:v1.1.0", cfg.PwruImage) + }) + + s.Run("handles partial config", func() { + tomlStr := `kernel_debug_image = "registry.example.com/toolbox:1.0"` + var cfg Config + err := toml.Unmarshal([]byte(tomlStr), &cfg) + s.NoError(err) + s.Equal("registry.example.com/toolbox:1.0", cfg.KernelDebugImage) + s.Equal("", cfg.TcpdumpImage) + s.Equal("", cfg.PwruImage) + }) +} + +func (s *ConfigSuite) TestConfigValidation() { + s.Run("validates all fields present", func() { + cfg := Config{ + KernelDebugImage: "toolbox:latest", + TcpdumpImage: "netshoot:latest", + PwruImage: "pwru:latest", + } + err := cfg.Validate() + s.NoError(err) + }) + + s.Run("validates partial config", func() { + cfg := Config{ + KernelDebugImage: "toolbox:latest", + } + err := cfg.Validate() + s.NoError(err) + }) + + s.Run("validates empty config", func() { + cfg := Config{} + err := cfg.Validate() + s.NoError(err) + }) +} + +func TestConfig(t *testing.T) { + suite.Run(t, new(ConfigSuite)) +} diff --git a/pkg/toolsets/cni-diagnostics/kernel/tools_test.go b/pkg/toolsets/cni-diagnostics/kernel/tools_test.go new file mode 100644 index 000000000..d2b64daef --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/kernel/tools_test.go @@ -0,0 +1,606 @@ +package kernel + +import ( + "context" + "strings" + "testing" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/config" + "github.com/stretchr/testify/suite" +) + +type ToolsSuite struct { + suite.Suite +} + +func TestTools(t *testing.T) { + suite.Run(t, new(ToolsSuite)) +} + +type mockToolCallRequest struct { + args map[string]any +} + +func (m *mockToolCallRequest) GetArguments() map[string]any { + return m.args +} + +func (s *ToolsSuite) handlerParams(args map[string]any) api.ToolHandlerParams { + return api.ToolHandlerParams{ + Context: context.Background(), + BaseConfig: config.BaseDefault(), + ToolCallRequest: &mockToolCallRequest{args: args}, + } +} + +func (s *ToolsSuite) assertReadOnlyToolAnnotations(tool api.ServerTool, title string) { + s.Equal(title, tool.Tool.Annotations.Title) + s.Require().NotNil(tool.Tool.Annotations.ReadOnlyHint) + s.True(*tool.Tool.Annotations.ReadOnlyHint) + s.Require().NotNil(tool.Tool.Annotations.DestructiveHint) + s.False(*tool.Tool.Annotations.DestructiveHint) + s.Require().NotNil(tool.Tool.Annotations.IdempotentHint) + s.True(*tool.Tool.Annotations.IdempotentHint) +} + +func (s *ToolsSuite) assertCommonOutputParams(schema *api.Tool) { + s.NotNil(schema.InputSchema.Properties["head"]) + s.Equal("integer", schema.InputSchema.Properties["head"].Type) + s.NotNil(schema.InputSchema.Properties["tail"]) + s.Equal("integer", schema.InputSchema.Properties["tail"].Type) + s.NotNil(schema.InputSchema.Properties["apply_tail_first"]) + s.Equal("boolean", schema.InputSchema.Properties["apply_tail_first"].Type) + s.NotNil(schema.InputSchema.Properties["timeout_seconds"]) + s.Equal("integer", schema.InputSchema.Properties["timeout_seconds"].Type) +} + +func (s *ToolsSuite) toolByName(name string) api.ServerTool { + s.T().Helper() + for _, tool := range InitKernelTools() { + if tool.Tool.Name == name { + return tool + } + } + s.T().Fatalf("expected tool %q in InitKernelTools()", name) + return api.ServerTool{} +} + +func (s *ToolsSuite) TestInitKernelTools() { + s.Run("returns four kernel tools", func() { + tools := InitKernelTools() + s.Len(tools, 4) + }) + + s.Run("registers tools with expected names in order", func() { + tools := InitKernelTools() + names := make([]string, len(tools)) + for i, tool := range tools { + names[i] = tool.Tool.Name + s.NotNil(tool.Handler, "tool %q should have a handler", tool.Tool.Name) + } + s.Equal([]string{"get-conntrack", "get-iptables", "get-nft", "get-ip"}, names) + }) +} + +func (s *ToolsSuite) TestInitConntrack() { + tool := s.toolByName("get-conntrack") + + s.Run("has correct name", func() { + s.Equal("get-conntrack", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(tool.Tool.Description, "connection tracking") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "node") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["node"].Type) + s.Equal("string", props["namespace"].Type) + s.Equal("string", props["command"].Type) + s.Equal("string", props["filter_parameters"].Type) + s.assertCommonOutputParams(&tool.Tool) + }) + + s.Run("command parameter has conntrack operations", func() { + enum := tool.Tool.InputSchema.Properties["command"].Enum + s.Len(enum, 6) + s.ElementsMatch([]any{"-L", "--dump", "-C", "--count", "-S", "--stats"}, enum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyToolAnnotations(tool, "CNI-Diagnostics: Kernel Connection Tracking") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestConntrackHandler() { + tool := s.toolByName("get-conntrack") + + s.Run("missing node returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "node parameter required") + }) + + s.Run("invalid node type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"node": 123})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "node parameter must be a string") + }) + + s.Run("invalid namespace type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "namespace": 123} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "namespace parameter must be a string") + }) + + s.Run("invalid command type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": 42} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "command parameter must be a string") + }) + + s.Run("invalid filter_parameters type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "filter_parameters": true} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "filter_parameters parameter must be a string") + }) + + s.Run("invalid head type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "head": "many"} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "head parameter must be an integer") + }) + + s.Run("invalid tail type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "tail": "many"} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "tail parameter must be an integer") + }) + + s.Run("invalid apply_tail_first type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "apply_tail_first": "yes"} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "apply_tail_first parameter must be a boolean") + }) + + s.Run("invalid timeout_seconds type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "timeout_seconds": "long"} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "timeout_seconds parameter must be an integer") + }) + + s.Run("negative head returns validation error", func() { + args := map[string]any{"node": "worker-0", "head": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("head must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative tail returns validation error", func() { + args := map[string]any{"node": "worker-0", "tail": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("tail must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative timeout_seconds returns validation error", func() { + args := map[string]any{"node": "worker-0", "timeout_seconds": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} + +func (s *ToolsSuite) TestInitIPtables() { + tool := s.toolByName("get-iptables") + + s.Run("has correct name", func() { + s.Equal("get-iptables", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(tool.Tool.Description, "packet filter") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "node") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["node"].Type) + s.Equal("string", props["namespace"].Type) + s.Equal("string", props["table"].Type) + s.Equal("string", props["command"].Type) + s.Equal("string", props["filter_parameters"].Type) + s.assertCommonOutputParams(&tool.Tool) + }) + + s.Run("table parameter has iptables tables", func() { + enum := tool.Tool.InputSchema.Properties["table"].Enum + s.Len(enum, 5) + s.ElementsMatch([]any{"filter", "nat", "mangle", "raw", "security"}, enum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyToolAnnotations(tool, "CNI-Diagnostics: Kernel IPtables") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestIPtablesHandler() { + tool := s.toolByName("get-iptables") + + s.Run("missing node returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "node parameter required") + }) + + s.Run("invalid node type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"node": true})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "node parameter must be a string") + }) + + s.Run("invalid namespace type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "namespace": 123} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "namespace parameter must be a string") + }) + + s.Run("invalid table type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "table": 8080} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "table parameter must be a string") + }) + + s.Run("invalid command type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": 42} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "command parameter must be a string") + }) + + s.Run("invalid filter_parameters type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "filter_parameters": []string{"-n"}} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "filter_parameters parameter must be a string") + }) + + s.Run("invalid head type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "head": "many"} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "head parameter must be an integer") + }) + + s.Run("negative head returns validation error", func() { + args := map[string]any{"node": "worker-0", "head": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("head must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative tail returns validation error", func() { + args := map[string]any{"node": "worker-0", "tail": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("tail must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative timeout_seconds returns validation error", func() { + args := map[string]any{"node": "worker-0", "timeout_seconds": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} + +func (s *ToolsSuite) TestInitNFT() { + tool := s.toolByName("get-nft") + + s.Run("has correct name", func() { + s.Equal("get-nft", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(strings.ToLower(tool.Tool.Description), "nftables") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "node") + s.Contains(tool.Tool.InputSchema.Required, "command") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["node"].Type) + s.Equal("string", props["namespace"].Type) + s.Equal("string", props["command"].Type) + s.Equal("string", props["address_families"].Type) + s.assertCommonOutputParams(&tool.Tool) + }) + + s.Run("command parameter has nft operations", func() { + enum := tool.Tool.InputSchema.Properties["command"].Enum + s.Len(enum, 6) + s.ElementsMatch([]any{"list ruleset", "list tables", "list chains", "list sets", "list maps", "list flowtables"}, enum) + }) + + s.Run("address_families parameter has supported families", func() { + enum := tool.Tool.InputSchema.Properties["address_families"].Enum + s.Len(enum, 6) + s.ElementsMatch([]any{"ip", "ip6", "inet", "arp", "bridge", "netdev"}, enum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyToolAnnotations(tool, "CNI-Diagnostics: Kernel NFtables") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestNFTHandler() { + tool := s.toolByName("get-nft") + + s.Run("missing node returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"command": "list ruleset"})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "node parameter required") + }) + + s.Run("missing command returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"node": "worker-0"})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "command parameter required") + }) + + s.Run("invalid command type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": []string{"list ruleset"}} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "command parameter must be a string") + }) + + s.Run("invalid namespace type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": "list ruleset", "namespace": 123} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "namespace parameter must be a string") + }) + + s.Run("invalid address_families type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": "list ruleset", "address_families": 4} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "address_families parameter must be a string") + }) + + s.Run("negative head returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "list ruleset", "head": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("head must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative tail returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "list ruleset", "tail": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("tail must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative timeout_seconds returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "list ruleset", "timeout_seconds": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} + +func (s *ToolsSuite) TestInitIP() { + tool := s.toolByName("get-ip") + + s.Run("has correct name", func() { + s.Equal("get-ip", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(tool.Tool.Description, "ip commands") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "node") + s.Contains(tool.Tool.InputSchema.Required, "command") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["node"].Type) + s.Equal("string", props["namespace"].Type) + s.Equal("string", props["options"].Type) + s.Equal("string", props["command"].Type) + s.Equal("string", props["filter_parameters"].Type) + s.assertCommonOutputParams(&tool.Tool) + }) + + s.Run("command parameter has ip subcommands", func() { + enum := tool.Tool.InputSchema.Properties["command"].Enum + s.Len(enum, 9) + s.ElementsMatch([]any{ + "address show", "link show", "neighbour show", "netns show", + "route show", "rule show", "vrf show", "xfrm state list", "xfrm policy list", + }, enum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyToolAnnotations(tool, "CNI-Diagnostics: Kernel IP Command") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestIPHandler() { + tool := s.toolByName("get-ip") + + s.Run("missing node returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"command": "route show"})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "node parameter required") + }) + + s.Run("missing command returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{"node": "worker-0"})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "command parameter required") + }) + + s.Run("invalid command type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": 42} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "command parameter must be a string") + }) + + s.Run("invalid namespace type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "namespace": 123} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "namespace parameter must be a string") + }) + + s.Run("invalid options type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "options": 4} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "options parameter must be a string") + }) + + s.Run("invalid filter_parameters type returns parameter error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "filter_parameters": true} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "filter_parameters parameter must be a string") + }) + + s.Run("negative head returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "head": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("head must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative tail returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "tail": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("tail must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("negative timeout_seconds returns validation error", func() { + args := map[string]any{"node": "worker-0", "command": "route show", "timeout_seconds": float64(-1)} + result, err := tool.Handler(s.handlerParams(args)) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} diff --git a/pkg/toolsets/cni-diagnostics/network-tools/tools_test.go b/pkg/toolsets/cni-diagnostics/network-tools/tools_test.go new file mode 100644 index 000000000..41e8f08da --- /dev/null +++ b/pkg/toolsets/cni-diagnostics/network-tools/tools_test.go @@ -0,0 +1,454 @@ +package network_tools + +import ( + "context" + "testing" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/config" + ovnknetmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/network-tools/mcp" + "github.com/stretchr/testify/suite" +) + +type ToolsSuite struct { + suite.Suite +} + +func TestTools(t *testing.T) { + suite.Run(t, new(ToolsSuite)) +} + +type mockToolCallRequest struct { + args map[string]any +} + +func (m *mockToolCallRequest) GetArguments() map[string]any { + return m.args +} + +func (s *ToolsSuite) handlerParams(args map[string]any) api.ToolHandlerParams { + return api.ToolHandlerParams{ + Context: context.Background(), + BaseConfig: config.BaseDefault(), + ToolCallRequest: &mockToolCallRequest{args: args}, + } +} + +func (s *ToolsSuite) assertReadOnlyNonIdempotentAnnotations(tool api.ServerTool, title string) { + s.Equal(title, tool.Tool.Annotations.Title) + s.Require().NotNil(tool.Tool.Annotations.ReadOnlyHint) + s.True(*tool.Tool.Annotations.ReadOnlyHint) + s.Require().NotNil(tool.Tool.Annotations.DestructiveHint) + s.False(*tool.Tool.Annotations.DestructiveHint) + s.Require().NotNil(tool.Tool.Annotations.IdempotentHint) + s.False(*tool.Tool.Annotations.IdempotentHint) +} + +func (s *ToolsSuite) toolByName(name string) api.ServerTool { + s.T().Helper() + for _, tool := range InitNetworkTools() { + if tool.Tool.Name == name { + return tool + } + } + s.T().Fatalf("expected tool %q in InitNetworkTools()", name) + return api.ServerTool{} +} + +func (s *ToolsSuite) TestInitNetworkTools() { + s.Run("returns two network tools", func() { + tools := InitNetworkTools() + s.Len(tools, 2) + }) + + s.Run("registers tools with expected names in order", func() { + tools := InitNetworkTools() + names := make([]string, len(tools)) + for i, tool := range tools { + names[i] = tool.Tool.Name + s.NotNil(tool.Handler, "tool %q should have a handler", tool.Tool.Name) + } + s.Equal([]string{"tcpdump", "pwru"}, names) + }) +} + +func (s *ToolsSuite) TestInitTcpdump() { + tool := s.toolByName("tcpdump") + + s.Run("has correct name", func() { + s.Equal("tcpdump", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(tool.Tool.Description, "Capture network packets") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "target_type") + s.Contains(tool.Tool.InputSchema.Required, "name") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["target_type"].Type) + s.Equal("string", props["name"].Type) + s.Equal("string", props["namespace"].Type) + s.Equal("string", props["container_name"].Type) + s.Equal("string", props["interface"].Type) + s.Equal("integer", props["packet_count"].Type) + s.Equal("string", props["bpf_filter"].Type) + s.Equal("integer", props["snaplen"].Type) + s.Equal("integer", props["timeout_seconds"].Type) + }) + + s.Run("target_type parameter has supported values", func() { + enum := tool.Tool.InputSchema.Properties["target_type"].Enum + s.Len(enum, 2) + s.ElementsMatch([]any{"node", "pod"}, enum) + }) + + s.Run("packet_count has expected bounds", func() { + prop := tool.Tool.InputSchema.Properties["packet_count"] + s.Require().NotNil(prop.Minimum) + s.Equal(float64(0), *prop.Minimum) + s.Require().NotNil(prop.Maximum) + s.Equal(float64(ovnknetmcp.MaxPacketCount), *prop.Maximum) + }) + + s.Run("snaplen has expected bounds", func() { + prop := tool.Tool.InputSchema.Properties["snaplen"] + s.Require().NotNil(prop.Minimum) + s.Equal(float64(0), *prop.Minimum) + s.Require().NotNil(prop.Maximum) + s.Equal(float64(ovnknetmcp.MaxSnaplen), *prop.Maximum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyNonIdempotentAnnotations(tool, "CNI-Diagnostics: Network Packet Capture") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestTcpdumpHandler() { + tool := s.toolByName("tcpdump") + + s.Run("missing target_type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "target_type parameter required") + }) + + s.Run("invalid target_type type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": 123, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "target_type parameter must be a string") + }) + + s.Run("missing name returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "name parameter required") + }) + + s.Run("invalid name type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": 42, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "name parameter must be a string") + }) + + s.Run("invalid target_type value returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "vm", + "name": "worker-0", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("target_type must be 'node' or 'pod'", result.Error.Error()) + }) + + s.Run("empty name returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("name is required when target_type is 'node'", result.Error.Error()) + }) + + s.Run("pod target without namespace returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "pod", + "name": "my-pod", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("namespace is required when target_type is 'pod'", result.Error.Error()) + }) + + s.Run("invalid namespace type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "pod", + "name": "my-pod", + "namespace": 123, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "namespace parameter must be a string") + }) + + s.Run("invalid container_name type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "pod", + "name": "my-pod", + "namespace": "default", + "container_name": 8080, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "container_name parameter must be a string") + }) + + s.Run("invalid interface type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "interface": true, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "interface parameter must be a string") + }) + + s.Run("invalid bpf_filter type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "bpf_filter": 8080, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "bpf_filter parameter must be a string") + }) + + s.Run("invalid packet_count type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "packet_count": "many", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "packet_count parameter must be an integer") + }) + + s.Run("negative packet_count returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "packet_count": float64(-1), + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("packet_count must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("invalid snaplen type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "snaplen": "large", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "snaplen parameter must be an integer") + }) + + s.Run("negative snaplen returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "snaplen": float64(-1), + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("snaplen must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("invalid timeout_seconds type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "timeout_seconds": "long", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "timeout_seconds parameter must be an integer") + }) + + s.Run("negative timeout_seconds returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "target_type": "node", + "name": "worker-0", + "timeout_seconds": float64(-1), + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} + +func (s *ToolsSuite) TestInitPwru() { + tool := s.toolByName("pwru") + + s.Run("has correct name", func() { + s.Equal("pwru", tool.Tool.Name) + }) + + s.Run("has description", func() { + s.NotEmpty(tool.Tool.Description) + s.Contains(tool.Tool.Description, "eBPF") + }) + + s.Run("has input schema", func() { + s.Require().NotNil(tool.Tool.InputSchema) + s.Equal("object", tool.Tool.InputSchema.Type) + s.Contains(tool.Tool.InputSchema.Required, "node_name") + }) + + s.Run("has expected parameters", func() { + props := tool.Tool.InputSchema.Properties + s.Equal("string", props["node_name"].Type) + s.Equal("string", props["node_pod_namespace"].Type) + s.Equal("string", props["bpf_filter"].Type) + s.Equal("integer", props["output_limit_lines"].Type) + s.Equal("integer", props["timeout_seconds"].Type) + }) + + s.Run("output_limit_lines has expected bounds", func() { + prop := tool.Tool.InputSchema.Properties["output_limit_lines"] + s.Require().NotNil(prop.Minimum) + s.Equal(float64(0), *prop.Minimum) + s.Require().NotNil(prop.Maximum) + s.Equal(float64(ovnknetmcp.MaxOutputLimitLines), *prop.Maximum) + }) + + s.Run("has annotations", func() { + s.assertReadOnlyNonIdempotentAnnotations(tool, "CNI-Diagnostics: Network eBPF Packet Tracing") + }) + + s.Run("has handler", func() { + s.NotNil(tool.Handler) + }) +} + +func (s *ToolsSuite) TestPwruHandler() { + tool := s.toolByName("pwru") + + s.Run("missing node_name returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{})) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "node_name parameter required") + }) + + s.Run("invalid node_name type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": 42, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "node_name parameter must be a string") + }) + + s.Run("invalid node_pod_namespace type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "node_pod_namespace": 123, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "node_pod_namespace parameter must be a string") + }) + + s.Run("invalid bpf_filter type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "bpf_filter": 8080, + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "bpf_filter parameter must be a string") + }) + + s.Run("invalid output_limit_lines type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "output_limit_lines": "many", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "output_limit_lines parameter must be an integer") + }) + + s.Run("negative output_limit_lines returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "output_limit_lines": float64(-1), + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("output_limit_lines must be greater than or equal to 0", result.Error.Error()) + }) + + s.Run("invalid timeout_seconds type returns parameter error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "timeout_seconds": "long", + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Contains(result.Error.Error(), "invalid parameters") + s.Contains(result.Error.Error(), "timeout_seconds parameter must be an integer") + }) + + s.Run("negative timeout_seconds returns validation error", func() { + result, err := tool.Handler(s.handlerParams(map[string]any{ + "node_name": "worker-0", + "timeout_seconds": float64(-1), + })) + s.Require().NoError(err) + s.Require().Error(result.Error) + s.Equal("timeout_seconds must be greater than or equal to 0", result.Error.Error()) + }) +} From 70f62d6b73491c1af49dd5e7b6abe7dc4a8b2d11 Mon Sep 17 00:00:00 2001 From: arkadeepsen Date: Thu, 23 Jul 2026 22:55:11 +0530 Subject: [PATCH 4/5] feat(cni-diagnostics): Add evals for kernel and network-tools Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: arkadeepsen Co-authored-by: Cursor --- .../acp-anthropic/eval-cni-diagnostics.yaml | 25 ++++++ .../acp-google/eval-cni-diagnostics.yaml | 25 ++++++ .../eval-cni-diagnostics.yaml | 25 ++++++ .../builtin-google/eval-cni-diagnostics.yaml | 25 ++++++ .../builtin-openai/eval-cni-diagnostics.yaml | 25 ++++++ .../kernel-conntrack-filter/cleanup.sh | 4 + .../kernel-conntrack-filter.yaml | 16 ++++ .../kernel-conntrack-filter/setup.sh | 13 +++ .../kernel-conntrack-list/cleanup.sh | 4 + .../kernel-conntrack-list.yaml | 16 ++++ .../kernel-conntrack-list/setup.sh | 15 ++++ .../kernel-ip-routes/cleanup.sh | 4 + .../kernel-ip-routes/kernel-ip-routes.yaml | 16 ++++ .../cni-diagnostics/kernel-ip-routes/setup.sh | 10 +++ .../kernel-iptables-list/cleanup.sh | 4 + .../kernel-iptables-list.yaml | 16 ++++ .../kernel-iptables-list/setup.sh | 11 +++ .../cleanup.sh | 5 ++ .../kernel-network-namespace-diagnosis.yaml | 25 ++++++ .../setup.sh | 43 ++++++++++ .../kernel-nft-list/cleanup.sh | 4 + .../kernel-nft-list/kernel-nft-list.yaml | 16 ++++ .../cni-diagnostics/kernel-nft-list/setup.sh | 10 +++ .../network-packet-drop-diagnosis/cleanup.sh | 5 ++ .../network-packet-drop-diagnosis.yaml | 23 +++++ .../network-packet-drop-diagnosis/setup.sh | 84 +++++++++++++++++++ .../network-pwru-trace/cleanup.sh | 5 ++ .../network-pwru-trace.yaml | 22 +++++ .../network-pwru-trace/setup.sh | 45 ++++++++++ .../network-tcpdump-basic/cleanup.sh | 5 ++ .../network-tcpdump-basic.yaml | 16 ++++ .../network-tcpdump-basic/setup.sh | 43 ++++++++++ 32 files changed, 605 insertions(+) create mode 100644 evals/core-eval-testing/acp-anthropic/eval-cni-diagnostics.yaml create mode 100644 evals/core-eval-testing/acp-google/eval-cni-diagnostics.yaml create mode 100644 evals/core-eval-testing/builtin-anthropic/eval-cni-diagnostics.yaml create mode 100644 evals/core-eval-testing/builtin-google/eval-cni-diagnostics.yaml create mode 100644 evals/core-eval-testing/builtin-openai/eval-cni-diagnostics.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-conntrack-filter/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-conntrack-filter/kernel-conntrack-filter.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-conntrack-filter/setup.sh create mode 100755 evals/tasks/cni-diagnostics/kernel-conntrack-list/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-conntrack-list/kernel-conntrack-list.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-conntrack-list/setup.sh create mode 100755 evals/tasks/cni-diagnostics/kernel-ip-routes/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-ip-routes/kernel-ip-routes.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-ip-routes/setup.sh create mode 100755 evals/tasks/cni-diagnostics/kernel-iptables-list/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-iptables-list/kernel-iptables-list.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-iptables-list/setup.sh create mode 100755 evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/kernel-network-namespace-diagnosis.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/setup.sh create mode 100755 evals/tasks/cni-diagnostics/kernel-nft-list/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/kernel-nft-list/kernel-nft-list.yaml create mode 100755 evals/tasks/cni-diagnostics/kernel-nft-list/setup.sh create mode 100755 evals/tasks/cni-diagnostics/network-packet-drop-diagnosis/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/network-packet-drop-diagnosis/network-packet-drop-diagnosis.yaml create mode 100755 evals/tasks/cni-diagnostics/network-packet-drop-diagnosis/setup.sh create mode 100755 evals/tasks/cni-diagnostics/network-pwru-trace/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/network-pwru-trace/network-pwru-trace.yaml create mode 100755 evals/tasks/cni-diagnostics/network-pwru-trace/setup.sh create mode 100755 evals/tasks/cni-diagnostics/network-tcpdump-basic/cleanup.sh create mode 100644 evals/tasks/cni-diagnostics/network-tcpdump-basic/network-tcpdump-basic.yaml create mode 100755 evals/tasks/cni-diagnostics/network-tcpdump-basic/setup.sh diff --git a/evals/core-eval-testing/acp-anthropic/eval-cni-diagnostics.yaml b/evals/core-eval-testing/acp-anthropic/eval-cni-diagnostics.yaml new file mode 100644 index 000000000..5be1edf7b --- /dev/null +++ b/evals/core-eval-testing/acp-anthropic/eval-cni-diagnostics.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "cni-diagnostics-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: cni-diagnostics + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 diff --git a/evals/core-eval-testing/acp-google/eval-cni-diagnostics.yaml b/evals/core-eval-testing/acp-google/eval-cni-diagnostics.yaml new file mode 100644 index 000000000..5be1edf7b --- /dev/null +++ b/evals/core-eval-testing/acp-google/eval-cni-diagnostics.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "cni-diagnostics-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: cni-diagnostics + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 diff --git a/evals/core-eval-testing/builtin-anthropic/eval-cni-diagnostics.yaml b/evals/core-eval-testing/builtin-anthropic/eval-cni-diagnostics.yaml new file mode 100644 index 000000000..5be1edf7b --- /dev/null +++ b/evals/core-eval-testing/builtin-anthropic/eval-cni-diagnostics.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "cni-diagnostics-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: cni-diagnostics + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 diff --git a/evals/core-eval-testing/builtin-google/eval-cni-diagnostics.yaml b/evals/core-eval-testing/builtin-google/eval-cni-diagnostics.yaml new file mode 100644 index 000000000..5be1edf7b --- /dev/null +++ b/evals/core-eval-testing/builtin-google/eval-cni-diagnostics.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "cni-diagnostics-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: cni-diagnostics + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 diff --git a/evals/core-eval-testing/builtin-openai/eval-cni-diagnostics.yaml b/evals/core-eval-testing/builtin-openai/eval-cni-diagnostics.yaml new file mode 100644 index 000000000..5be1edf7b --- /dev/null +++ b/evals/core-eval-testing/builtin-openai/eval-cni-diagnostics.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "cni-diagnostics-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: cni-diagnostics + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-filter/cleanup.sh b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/cleanup.sh new file mode 100755 index 000000000..95db139ca --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/cleanup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl delete pods -A -l app.kubernetes.io/component=node-debug --ignore-not-found --wait=true --timeout=120s diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-filter/kernel-conntrack-filter.yaml b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/kernel-conntrack-filter.yaml new file mode 100644 index 000000000..420bfa042 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/kernel-conntrack-filter.yaml @@ -0,0 +1,16 @@ +kind: Task +metadata: + labels: + suite: cni-diagnostics + category: kernel + name: "kernel-conntrack-filter" + difficulty: medium +steps: + setup: + file: setup.sh + verify: + contains: "The agent retrieved filtered conntrack entries for TCP connections to the Kubernetes API server on destination port 443 or 6443" + cleanup: + file: cleanup.sh + prompt: + inline: "Find connection tracking entries on a worker node for TCP connections to the Kubernetes API server (typically port 443 or 6443). Filter the conntrack table to show only these specific connections." diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-filter/setup.sh b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/setup.sh new file mode 100755 index 000000000..34ed4b1f0 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-filter/setup.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +if ! kubectl get nodes -l '!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' --no-headers | grep -q .; then + echo "Error: No non-control-plane nodes found" + exit 1 +fi + +# Get API server endpoint to verify there should be connections +API_SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') +echo "API server: $API_SERVER" +echo "Setup complete" +exit 0 diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-list/cleanup.sh b/evals/tasks/cni-diagnostics/kernel-conntrack-list/cleanup.sh new file mode 100755 index 000000000..95db139ca --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-list/cleanup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl delete pods -A -l app.kubernetes.io/component=node-debug --ignore-not-found --wait=true --timeout=120s diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-list/kernel-conntrack-list.yaml b/evals/tasks/cni-diagnostics/kernel-conntrack-list/kernel-conntrack-list.yaml new file mode 100644 index 000000000..ac1c3684c --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-list/kernel-conntrack-list.yaml @@ -0,0 +1,16 @@ +kind: Task +metadata: + labels: + suite: cni-diagnostics + category: kernel + name: "kernel-conntrack-list" + difficulty: easy +steps: + setup: + file: setup.sh + verify: + contains: "The agent retrieved conntrack entries from a worker node showing TCP or UDP connections with source/destination IPs and connection states, including ESTABLISHED connections" + cleanup: + file: cleanup.sh + prompt: + inline: "List all active connection tracking entries on the Kubernetes worker node. Show connections in the ESTABLISHED state. The cluster has OVN-Kubernetes as the CNI." diff --git a/evals/tasks/cni-diagnostics/kernel-conntrack-list/setup.sh b/evals/tasks/cni-diagnostics/kernel-conntrack-list/setup.sh new file mode 100755 index 000000000..86c700aa0 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-conntrack-list/setup.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +# This task runs against a cluster with OVN-Kubernetes CNI +# No specific setup needed - we're just reading conntrack state +# The cluster should have at least one non-control-plane node + +# Verify we have at least one non-control-plane node +if ! kubectl get nodes -l '!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' --no-headers | grep -q .; then + echo "Error: No non-control-plane nodes found. This task requires a cluster with non-control-plane nodes." + exit 1 +fi + +echo "Setup complete: cluster has non-control-plane nodes for conntrack testing" +exit 0 diff --git a/evals/tasks/cni-diagnostics/kernel-ip-routes/cleanup.sh b/evals/tasks/cni-diagnostics/kernel-ip-routes/cleanup.sh new file mode 100755 index 000000000..95db139ca --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-ip-routes/cleanup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl delete pods -A -l app.kubernetes.io/component=node-debug --ignore-not-found --wait=true --timeout=120s diff --git a/evals/tasks/cni-diagnostics/kernel-ip-routes/kernel-ip-routes.yaml b/evals/tasks/cni-diagnostics/kernel-ip-routes/kernel-ip-routes.yaml new file mode 100644 index 000000000..367b69709 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-ip-routes/kernel-ip-routes.yaml @@ -0,0 +1,16 @@ +kind: Task +metadata: + labels: + suite: cni-diagnostics + category: kernel + name: "kernel-ip-routes" + difficulty: easy +steps: + setup: + file: setup.sh + verify: + contains: "The agent retrieved routes from all routing tables on a worker node, not only the main table, including table names or numbers and default routes" + cleanup: + file: cleanup.sh + prompt: + inline: "Display the routing table on a Kubernetes worker node. Show all routing tables, not just the main table." diff --git a/evals/tasks/cni-diagnostics/kernel-ip-routes/setup.sh b/evals/tasks/cni-diagnostics/kernel-ip-routes/setup.sh new file mode 100755 index 000000000..ac7b65101 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-ip-routes/setup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +if ! kubectl get nodes -l '!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' --no-headers | grep -q .; then + echo "Error: No non-control-plane nodes found" + exit 1 +fi + +echo "Setup complete" +exit 0 diff --git a/evals/tasks/cni-diagnostics/kernel-iptables-list/cleanup.sh b/evals/tasks/cni-diagnostics/kernel-iptables-list/cleanup.sh new file mode 100755 index 000000000..95db139ca --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-iptables-list/cleanup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl delete pods -A -l app.kubernetes.io/component=node-debug --ignore-not-found --wait=true --timeout=120s diff --git a/evals/tasks/cni-diagnostics/kernel-iptables-list/kernel-iptables-list.yaml b/evals/tasks/cni-diagnostics/kernel-iptables-list/kernel-iptables-list.yaml new file mode 100644 index 000000000..ad39ada4d --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-iptables-list/kernel-iptables-list.yaml @@ -0,0 +1,16 @@ +kind: Task +metadata: + labels: + suite: cni-diagnostics + category: kernel + name: "kernel-iptables-list" + difficulty: easy +steps: + setup: + file: setup.sh + verify: + contains: "The agent retrieved iptables NAT rules with packet and byte counters, including chain names and target rules" + cleanup: + file: cleanup.sh + prompt: + inline: "Show the iptables NAT rules on a Kubernetes worker node. Include packet and byte counters in the output." diff --git a/evals/tasks/cni-diagnostics/kernel-iptables-list/setup.sh b/evals/tasks/cni-diagnostics/kernel-iptables-list/setup.sh new file mode 100755 index 000000000..071396531 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-iptables-list/setup.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +# Verify we have at least one non-control-plane node +if ! kubectl get nodes -l '!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' --no-headers | grep -q .; then + echo "Error: No non-control-plane nodes found" + exit 1 +fi + +echo "Setup complete: cluster has non-control-plane nodes for iptables testing" +exit 0 diff --git a/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/cleanup.sh b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/cleanup.sh new file mode 100755 index 000000000..71e2cbe3c --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/cleanup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl delete pods -A -l app.kubernetes.io/component=node-debug --ignore-not-found --wait=true --timeout=120s +kubectl delete namespace netns-test --ignore-not-found diff --git a/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/kernel-network-namespace-diagnosis.yaml b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/kernel-network-namespace-diagnosis.yaml new file mode 100644 index 000000000..e9077e23f --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/kernel-network-namespace-diagnosis.yaml @@ -0,0 +1,25 @@ +kind: Task +metadata: + labels: + suite: cni-diagnostics + category: kernel + name: "kernel-network-namespace-diagnosis" + difficulty: hard +steps: + setup: + file: setup.sh + verify: + contains: "The agent diagnosed the external connectivity issue by analyzing routing tables, iptables POSTROUTING NAT rules, and nftables configuration, identifying a likely cause such as default gateway, MASQUERADE rules, or egress filtering" + cleanup: + file: cleanup.sh + prompt: + inline: | + A pod in namespace 'netns-test' on a worker node can't reach external networks. + Internal cluster networking works fine, but external DNS and internet connectivity fails. + + Diagnose the issue by checking: + 1. The routing table on the node (especially default gateway and external routes) + 2. NAT rules in iptables that handle outbound traffic (POSTROUTING chain) + 3. Any nftables configuration that might affect egress traffic + + Identify what's preventing external connectivity. diff --git a/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/setup.sh b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/setup.sh new file mode 100755 index 000000000..2bf103d58 --- /dev/null +++ b/evals/tasks/cni-diagnostics/kernel-network-namespace-diagnosis/setup.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -e + +WORKER_NODE=$(kubectl get nodes -l '!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' -o jsonpath='{.items[0].metadata.name}') +if [ -z "$WORKER_NODE" ]; then + echo "Error: No non-control-plane nodes found" + exit 1 +fi + +# Create test namespace and pod +kubectl delete namespace netns-test --ignore-not-found +kubectl create namespace netns-test + +cat < Date: Thu, 23 Jul 2026 23:44:07 +0530 Subject: [PATCH 5/5] update README.md and configuration.md files Signed-off-by: arkadeepsen --- README.md | 795 ++++++++++++++++++++++++++++++++++++------ docs/configuration.md | 60 +++- 2 files changed, 737 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index b90da160f..96b85b600 100644 --- a/README.md +++ b/README.md @@ -287,14 +287,25 @@ The following sets of tools are available (toolsets marked with ✓ in the Defau | Toolset | Description | Default | |-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | -| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | -| helm | Tools for managing Helm charts and releases | | -| kcp | Manage kcp workspaces and multi-tenancy features | | -| kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | | -| kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | | -| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | | -| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, TaskRuns, and troubleshooting. | | +| cluster-diagnostics | Tools for cluster diagnostics and troubleshooting | | +| cni-diagnostics | Tools for Container Network Interface (CNI) diagnostics and troubleshooting | | +| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | +| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | +| helm | Tools for managing Helm charts and releases | | +| kcp | Manage kcp workspaces and multi-tenancy features | | +| kubevirt | OpenShift Virtualization tools for managing virtual machines, check the [OpenShift Virtualization documentation](https://github.com/openshift/openshift-mcp-server/blob/main/docs/kubevirt.md) for more details. | | +| netedge | NetEdge troubleshooting tools for OpenShift | | +| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | | +| oadp | OADP (OpenShift API for Data Protection) tools for managing Velero backups, restores, and schedules | | +| observability/logs | Toolset for querying Loki logs | | +| observability/metrics | Toolset for querying Prometheus and Alertmanager endpoints in efficient ways. | | +| observability/otelcol | Toolset for OpenTelemetry Collector configuration assistance including schema validation, component documentation, and version management. | | +| observability/traces | Distributed tracing tools for discovering Tempo instances, searching and retrieving traces, and exploring trace attributes. | | +| openshift | OpenShift-specific tools for cluster management and troubleshooting | | +| openshift/mustgather | Analyze OpenShift must-gather archives offline without a live cluster connection | | +| ossm | Most common tools for managing OSSM, check the [OSSM documentation](https://github.com/openshift/openshift-mcp-server/blob/main/docs/OSSM.md) for more details. | | +| ovn-kubernetes | OVN-Kubernetes CNI network troubleshooting tools | | +| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, TaskRuns, and troubleshooting. | | @@ -319,6 +330,123 @@ In case multi-cluster support is enabled (default) and you have access to multip
+cni-diagnostics + +- **get-conntrack** - Interact with the connection tracking system on a Kubernetes node. Lists, counts, or shows statistics for tracked connections. Connection tracking shows active network connections and their state (ESTABLISHED, TIME_WAIT, etc.). + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `command` (`string`) - These options specify the particular operation to perform. These options can only be used if configured image has 'conntrack' utility available. + -L, --dump : List connection tracking table. + -C, --count: Show the table counter. + -S, --stats: Show the in-kernel connection tracking system statistics. + - `filter_parameters` (`string`) - These parameters are useful to filter certain entries from the whole table: + -s, --src, --orig-src IP_ADDRESS : Match only entries whose source address in the original direction equals to mentioned IP. + -d, --dst, --orig-dst IP_ADDRESS : Match only entries whose destination address in the original direction equals to mentioned IP. + -p, --proto PROTO : Specify layer four (TCP, UDP, ...) protocol. + --sport, --orig-port-src PORT : Source port in original direction. + --dport, --orig-port-dst PORT : Destination port in original direction. + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `namespace` (`string`) - Namespace of the debug pod from where conntrack entries are expected to be extracted (optional, defaults to 'default') + - `node` (`string`) **(required)** - Name of the node from where conntrack entries are expected to be extracted + - `tail` (`integer`) - Return only last N lines + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +- **get-iptables** - List packet filter rules using iptables or ip6tables on a Kubernetes node. Shows rules for specific tables (filter, nat, mangle, raw, security). Use this to inspect firewall rules, NAT configuration, and packet filtering on nodes. + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `command` (`string`) - These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + -L, --list [chain] : List all rules in the selected chain. If no chain is selected, all chains are listed. + -S, --list-rules [chain] : Print all rules in the selected chain. If no chain is selected, all chains are printed like iptables-save. + - `filter_parameters` (`string`) - These parameters are useful to filter certain entries from the whole table: + -s, --source address[/mask] : Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. + -d, --destination address[/mask] : Destination specification. + -v, --verbose : Verbose output. + -n, --numeric : Numeric output. IP addresses and port numbers will be printed in numeric format. + -p, --protocol protocol : The protocol of the rule or of the packet to check. + -4, --ipv4 : IPv4 + -6, --ipv6 : IPv6 + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `namespace` (`string`) - Namespace of the debug pod from where packet filter rules are expected to be extracted (optional, defaults to 'default') + - `node` (`string`) **(required)** - Name of the node from where packet filter rules are expected to be extracted + - `table` (`string`) - There are currently five independent tables (which tables are present at any time depends on the kernel configuration options and which modules are present). + filter : This is the default table + nat : This table is consulted when a packet that creates a new connection is encountered. + mangle : This table is used for specialized packet alteration. + raw : This table is used mainly for configuring exemptions from connection tracking in combination with the NOTRACK target. + security: This table is used for Mandatory Access Control (MAC) networking rules. + - `tail` (`integer`) - Return only last N lines + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +- **get-nft** - List nftables packet filtering and classification rules on a Kubernetes node. nftables is the modern replacement for iptables. Use this to inspect firewall rules, packet filtering, and network address translation. + - `address_families` (`string`) - Address families determine the type of packets which are processed. For each address family, the kernel contains so called hooks at specific stages of + the packet processing paths, which invoke nftables if rules for these hooks exist. + - ip IPv4 address family. + - ip6 IPv6 address family. + - inet Internet (IPv4/IPv6) address family. + - arp ARP address family, handling IPv4 ARP packets. + - bridge Bridge address family, handling packets which traverse a bridge device. + - netdev Netdev address family, handling packets on ingress and egress. + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `command` (`string`) **(required)** - These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - list ruleset : The ruleset keyword is used to identify the whole set of tables, chains, etc. Print the ruleset in human-readable format. + - list tables : List all chains and rules of the specified table. + - list chains : List all rules of the specified chain. + - list sets : Display the elements in the specified set. + - list maps : Display the elements in the specified map. + - list flowtables: List all flowtables. + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `namespace` (`string`) - Namespace of the debug pod from where packet filtering and classification rules are expected to be extracted (optional, defaults to 'default') + - `node` (`string`) **(required)** - Name of the node from where packet filtering and classification rules are expected to be extracted + - `tail` (`integer`) - Return only last N lines + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +- **get-ip** - Execute ip commands on a Kubernetes node to show routing, network devices, interfaces, and network namespaces. Part of the iproute2 suite for network configuration inspection. + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `command` (`string`) **(required)** - These options specify the desired action to perform. Only one of them can be specified on the command line unless otherwise stated below. + - address show : protocol (IP or IPv6) address on a device. + - link show : network device. + - neighbour show : manage ARP or NDISC cache entries. + - netns show : manage network namespaces. + - route show : routing table entry. + - rule show : rule in routing policy database. + - vrf show : manage virtual routing and forwarding devices. + - xfrm state list : show Security Association Database. + - xfrm policy list : show Security Policy Database. + - `filter_parameters` (`string`) - This allows to mention sub command to get more filtered data. Available sub command varies and supportability depends on what is + already supported with 'ip' utility. + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `namespace` (`string`) - Namespace of the debug pod on which ip command is expected to be executed (optional, defaults to 'default') + - `node` (`string`) **(required)** - Name of the node on which ip command is expected to be executed + - `options` (`string`) - These options helps in providing more details or formattig output data. + -d, -details : Output more detailed information. + -4 : shortcut for -family inet. + -6 : shortcut for -family inet6. + -r, -resolve : use the system's name resolver to print DNS names instead of host addresses. + -n, -netns : switches ip to the specified network namespace NETNS. + -a, -all : executes specified command over all objects, it depends if command supports this option. + - `tail` (`integer`) - Return only last N lines + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +- **tcpdump** - Capture network packets on a node or inside a pod with BPF filtering. Creates a specialized debug pod for node-level captures. IMPORTANT: Use restrictive BPF filters and low packet counts to avoid performance impact. Maximum 1000 packets. + - `bpf_filter` (`string`) - BPF filter expression (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1') + - `container_name` (`string`) - Name of the container in the pod when target_type is 'pod' (optional, uses default container if not specified) + - `interface` (`string`) - Network interface name or 'any' (optional, captures on all interfaces if not specified) + - `name` (`string`) **(required)** - Name of the target (node or pod) + - `namespace` (`string`) - Namespace of the target (node or pod). Required when target_type is 'pod'. Optional when target_type is 'node' and defaults to 'default'. + - `packet_count` (`integer`) - Number of packets to capture (default: 100, max: 1000) + - `snaplen` (`integer`) - Snapshot length in bytes (default: 96, max: 1500). Use 96 for headers only, 1500 for full packets. + - `target_type` (`string`) **(required)** - Capture target: 'node' (node-level) or 'pod' (pod network namespace) + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +- **pwru** - Trace packets through the Linux kernel networking stack using eBPF. pwru (packet, where are you?) shows which kernel functions process a packet, helping debug packet drops and routing issues. Creates a specialized debug pod with eBPF capabilities. + - `bpf_filter` (`string`) - BPF filter expression to match packets (optional, e.g., 'tcp and dst port 8080', 'host 10.0.0.1') + - `node_name` (`string`) **(required)** - Name of the node to run pwru on + - `node_pod_namespace` (`string`) - Namespace of the debug pod on which the command is expected to be executed (optional, defaults to 'default') + - `output_limit_lines` (`integer`) - Maximum number of trace events to capture (default: 100, max: 1000) + - `timeout_seconds` (`integer`) - Timeout in seconds for the command execution. If not specified, server default timeout is used. The maximum value is 300 seconds. + +
+ +
+ config - **configuration_contexts_list** - List all available context names and associated server urls from the kubeconfig file @@ -466,98 +594,14 @@ In case multi-cluster support is enabled (default) and you have access to multip
-kiali - -- **kiali_get_mesh_traffic_graph** - Returns service-to-service traffic topology, dependencies, and network metrics (throughput, response time, mTLS) for the specified namespaces. Use this to diagnose routing issues, latency, or find upstream/downstream dependencies. - - `graphType` (`string`) - Granularity of the graph. 'app' aggregates by app name, 'versionedApp' separates by versions, 'workload' maps specific pods/deployments. Default: versionedApp. - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespaces` (`string`) **(required)** - Comma-separated list of namespaces to map - -- **kiali_get_mesh_status** - Retrieves the high-level health, topology, and environment details of the Istio service mesh. Returns multi-cluster control plane status (istiod), data plane namespace health (including ambient mesh status), observability stack health (Prometheus, Grafana...), and component connectivity. Use this tool as the first step to diagnose mesh-wide issues, verify Istio/Kiali versions, or check overall health before drilling into specific workloads. - -- **kiali_manage_istio_config_read** - Read Istio, Gateway API, and Inference API config. 'list' groups by namespace→'group/version/kind'→{valid:[...],invalid:[...]} where valid/invalid arrays contain resource names; omit group/kind to retrieve ALL config types in a single call. Supports Istio (networking.istio.io, security.istio.io), Gateway API (gateway.networking.k8s.io), and Inference API (inference.networking.k8s.io) when installed. 'get' returns full YAML. For writes use manage_istio_config. - - `action` (`string`) **(required)** - Action to perform (read-only) - - `group` (`string`) - API group of the Istio object. Required ONLY for 'get' action. For 'list', OMIT group and kind to retrieve ALL config types in a single call. Use 'gateway.networking.k8s.io' for Gateway API resources. Use 'inference.networking.k8s.io' for Inference API resources. - - `kind` (`string`) - Kind of the Istio object. Required ONLY for 'get' action. For 'list', OMIT to return all kinds at once — do NOT call separately for each kind. - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespace` (`string`) - Namespace containing the Istio object. For 'list', if not provided, returns objects across all namespaces. For 'get', required. - - `object` (`string`) - Name of the Istio object. Required for 'get' action. - - `serviceName` (`string`) - Filter Istio configurations (VirtualServices, DestinationRules, and their referenced Gateways) that affect a specific service. Only applicable for 'list' action - - `version` (`string`) - API version. Use 'v1' for all resource types. Required for 'get' action. - -- **kiali_manage_istio_config** - Create, patch, or delete Istio, Gateway API, and Inference API config. Supports Istio resources (networking.istio.io, security.istio.io), Gateway API resources (gateway.networking.k8s.io), and Inference API resources (inference.networking.k8s.io) when installed on the cluster. For list and get (read-only) use manage_istio_config_read. - - `action` (`string`) **(required)** - Action to perform (write) - - `data` (`string`) - JSON or YAML data for the resource. Required for create and patch actions. For create, you can provide partial content (e.g. only spec) and it will be merged onto a valid template with defaults. Arrays (like servers, http, etc.) are REPLACED entirely, so include ALL elements you want. - - `group` (`string`) **(required)** - API group of the Istio object. Use 'gateway.networking.k8s.io' for Gateway API resources. Use 'inference.networking.k8s.io' for Inference API resources. - - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'VirtualService', 'DestinationRule'). - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespace` (`string`) **(required)** - Namespace containing the Istio object. - - `object` (`string`) **(required)** - Name of the Istio object. - - `version` (`string`) **(required)** - API version. Use 'v1' for all resource types. - -- **kiali_list_mesh_clusters** - Returns the list of Istio mesh clusters that Kiali can access. Each entry includes its name and whether it is the home cluster (where Kiali is deployed). Call this tool before using meshCluster on other Kiali tools when the target cluster is unknown. - -- **kiali_get_resource_details** - Fetches a list of resources OR retrieves detailed data for a specific resource. If 'resourceName' is omitted, it returns a list. If 'resourceName' is provided, it returns details for that specific resource. - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespaces` (`string`) - Comma-separated list of namespaces to query (e.g., 'bookinfo' or 'bookinfo,default'). If not provided, it will query across all accessible namespaces. - - `resourceName` (`string`) - Optional. The specific name of the resource. If left empty, the tool returns a list of all resources of the specified type. If provided, the tool returns deep details for this specific resource. - - `resourceType` (`string`) **(required)** - The type of resource to query. Use 'app' for Kiali applications (grouped by the Kubernetes 'app' label). Use 'argoapp' for ArgoCD Application CRDs (requires ArgoCD installed and the Kiali service account must have read permissions on applications.argoproj.io). - -- **kiali_list_traces** - Lists distributed traces for a service in a namespace. Returns a summary (namespace, service, total_found, avg_duration_ms) and a list of traces with id, duration_ms, spans_count, root_op, slowest_service, has_errors. Use get_trace_details with a trace id to get full hierarchy. - - `errorOnly` (`boolean`) - If true, only consider traces that contain errors. Default false. - - `limit` (`integer`) - Maximum number of traces to return. Default 10. - - `lookbackSeconds` (`integer`) - How far back to search. Default 600 (10m). - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespace` (`string`) **(required)** - Kubernetes namespace of the service. - - `serviceName` (`string`) **(required)** - Service name to search traces for (required). Returns multiple traces up to limit. - -- **kiali_get_trace_details** - Fetches a single distributed trace by trace_id and returns its call hierarchy (service tree with duration, status, and nested calls). Use this after list_traces to drill into a specific trace. - - `traceId` (`string`) **(required)** - Trace ID to fetch and summarize. If provided, namespace/service_name are ignored. - -- **kiali_get_pod_performance** - Returns a human-readable text summary with current Pod CPU/memory usage (from Prometheus) compared to Kubernetes requests/limits (from the Pod spec). Useful to answer questions like 'Is this workload using too much memory?' - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespace` (`string`) **(required)** - Kubernetes namespace of the Pod. - - `podName` (`string`) - Kubernetes Pod name. If workloadName is provided, the tool will attempt to resolve a Pod from that workload first. - - `queryTime` (`string`) - Optional end timestamp (RFC3339) for the query. Defaults to now. - - `timeRange` (`string`) - Time window used to compute CPU rate (Prometheus duration like '5m', '10m', '1h', '1d'). Defaults to '10m'. - - `workloadName` (`string`) - Kubernetes Workload name (e.g. Deployment/StatefulSet/etc). Tool will look up the workload and pick one of its Pods. If not found, it will fall back to treating this value as a podName. - -- **kiali_get_logs** - Get the logs of a Kubernetes Pod (or workload name that will be resolved to a pod) in a namespace. Output is plain text, matching kubernetes-mcp-server pods_log. The line_count field tells you the total number of log lines returned. Analyze ALL of them, but summarize the results unless the user explicitly asks for the raw output. Do not omit any error or warning lines. - - `container` (`string`) - Optional. Name of the Pod container to get the logs from. - - `format` (`string`) - Output formatting for chat. 'codeblock' wraps logs in ~~~ fences (recommended). 'plain' returns raw text like kubernetes-mcp-server pods_log. - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `name` (`string`) **(required)** - Name of the Pod to get the logs from. If it does not exist, it will be treated as a workload name and a running pod will be selected. - - `namespace` (`string`) **(required)** - Namespace to get the Pod logs from - - `previous` (`boolean`) - Optional. Return previous terminated container logs - - `severity` (`string`) - Optional severity filter applied client-side. Accepts 'ERROR', 'WARN' or combinations like 'ERROR,WARN'. - - `tail` (`integer`) - Number of lines to retrieve from the end of the logs (Optional, defaults to 50). Cannot exceed 200 lines. - - `workload` (`string`) - Optional. Workload name override (used when name lookup fails). - -- **kiali_get_metrics** - Returns a compact JSON summary of Istio metrics (latency quantiles, traffic trends, throughput, payload sizes) for the given resource. - - `byLabels` (`string`) - Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional - - `direction` (`string`) - Traffic direction. Optional, defaults to 'outbound' - - `meshCluster` (`string`) - Optional Istio mesh cluster name from kiali_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. - - `namespace` (`string`) **(required)** - Namespace to get metrics from - - `quantiles` (`string`) - Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional - - `rateInterval` (`string`) - Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '10m' - - `reporter` (`string`) - Metrics reporter(s). Comma-separated list of: 'source', 'destination', 'waypoint', or the special value 'both' (no reporter filter). Optional, defaults to 'source'. Example: 'source,waypoint' - - `requestProtocol` (`string`) - Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional - - `resourceName` (`string`) **(required)** - Name of the resource to get metrics for - - `resourceType` (`string`) **(required)** - Type of resource to get metrics - - `step` (`string`) - Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds - -
- -
- kubevirt -- **vm_clone** - Clone a VirtualMachine on KubeVirt by creating a VirtualMachineClone resource. This creates a copy of the source VM with a new name using the KubeVirt Clone API +- **vm_clone** - Clone a VirtualMachine on OpenShift Virtualization by creating a VirtualMachineClone resource. This creates a copy of the source VM with a new name using the OpenShift Virtualization Clone API - `name` (`string`) **(required)** - The name of the source virtual machine to clone - `namespace` (`string`) **(required)** - The namespace of the source virtual machine - `targetName` (`string`) **(required)** - The name for the new cloned virtual machine -- **vm_create** - Create a VirtualMachine on KubeVirt with the specified configuration, automatically resolving instance types, preferences, and container disk images. VM will be created in Halted state by default; use autostart parameter to start it immediately. +- **vm_create** - Create a VirtualMachine on OpenShift Virtualization with the specified configuration, automatically resolving instance types, preferences, and container disk images. VM will be created in Halted state by default; use autostart parameter to start it immediately. - `autostart` (`boolean`) - Optional flag to automatically start the VM after creation (sets runStrategy to Always instead of Halted). Defaults to false. - `instancetype` (`string`) - Optional instance type name for the VM (e.g., 'u1.small', 'u1.medium', 'u1.large') - `name` (`string`) **(required)** - The name of the virtual machine @@ -583,6 +627,55 @@ In case multi-cluster support is enabled (default) and you have access to multip
+netedge + +- **netedge_query_prometheus** - Executes specialized diagnostic queries for specific NetEdge components (ingress, dns). + - `diagnostic_target` (`string`) **(required)** - Run specialized diagnostics for a specific component. + +- **get_coredns_config** - Retrieve the current CoreDNS configuration (Corefile) from the cluster. + +- **get_service_endpoints** - Return EndpointSlice objects for a Service to verify backend pod availability. + - `namespace` (`string`) **(required)** - Service namespace + - `service` (`string`) **(required)** - Service name + +- **probe_dns_local** - Run a DNS query using local libraries on the MCP server host to verify connectivity and resolution. + - `name` (`string`) **(required)** - FQDN to query + - `server` (`string`) **(required)** - DNS server IP (e.g. 8.8.8.8, 10.0.0.10) + - `type` (`string`) - Record type (A, AAAA, CNAME, TXT, SRV, etc.). Defaults to A. + +- **probe_http** - Send an HTTP(S) request from the MCP server host to verify reachability and inspect the response status code and headers. + - `method` (`string`) - HTTP method to use. Defaults to GET. + - `timeout_seconds` (`integer`) - Request timeout in seconds. Defaults to 5. + - `url` (`string`) **(required)** - The URL to probe (e.g. https://example.com/path). + +- **inspect_route** - Inspect an OpenShift Route to view its full configuration and status. + - `namespace` (`string`) **(required)** - Route namespace + - `route` (`string`) **(required)** - Route name + +- **exec_dns_in_pod** - Spin up a temporary pod in the cluster to execute a DNS lookup using dig, verifying internal cluster networking and DNS path. + - `namespace` (`string`) **(required)** - Namespace to run the ephemeral pod in. + - `record_type` (`string`) - DNS record type (A, AAAA, etc.). Defaults to A. + - `target_name` (`string`) **(required)** - DNS name to query (e.g. kubernetes.default.svc.cluster.local). + - `target_server` (`string`) **(required)** - DNS server IP to query (e.g. 172.30.0.10). + +- **get_router_config** - Retrieve the current router's HAProxy configuration from the cluster. Supports filtering by section type (global/defaults/frontend/backend), substring filter on section headers, and line-count limiting via tail_lines. + - `filter` (`string`) - Substring filter applied to section headers (e.g. a route or backend name). Only sections whose header contains this string are returned. + - `pod` (`string`) - Router pod name (optional, chooses any existing if not provided) + - `section` (`string`) - Filter to a specific HAProxy config section type + - `tail_lines` (`integer`) - Maximum number of lines to return from the end of the config output (default: 200) + +- **get_router_info** - Retrieve HAProxy runtime information from the router. + - `pod` (`string`) - Router pod name (optional, chooses any existing if not provided) + +- **get_router_sessions** - Retrieve active sessions from the router. Supports limiting the number of sessions returned and filtering by substring (e.g. backend name or source IP). + - `filter` (`string`) - Substring filter applied to each session block. Only sessions containing this string are returned (e.g. a backend name or source IP). + - `limit` (`integer`) - Maximum number of session blocks to return (default: 50) + - `pod` (`string`) - Router pod name (optional, chooses any existing if not provided) + +
+ +
+ netobserv - **netobserv_list_flows** - Lists NetObserv network flow records from Loki. Use when investigating traffic between workloads, IPs, ports, or protocols in a namespace or time window. @@ -785,6 +878,12 @@ Examples:
+oadp + +
+ +
+ observability/logs - **loki_list_instances** - List LokiStack instances available in the Kubernetes cluster. @@ -1135,6 +1234,465 @@ Use tempo_search_tags to discover available tag names.
+openshift + +
+ +
+ +openshift/mustgather + +- **mustgather_use** - Load a must-gather archive from a given filesystem path for analysis. Must be called before any other mustgather_* tools. + - `path` (`string`) **(required)** - Absolute path to the must-gather archive directory + +- **mustgather_resources_list** - List Kubernetes resources from the must-gather archive with optional filtering by namespace, labels, and fields + - `apiVersion` (`string`) - API version (default: v1) + - `fieldSelector` (`string`) - Field selector (e.g., metadata.name=foo) + - `kind` (`string`) **(required)** - Resource kind (e.g., Pod, Deployment, Service) + - `labelSelector` (`string`) - Label selector (e.g., app=nginx,tier=frontend) + - `limit` (`integer`) - Maximum number of resources to return (0 for all) + - `namespace` (`string`) - Filter by namespace + +- **mustgather_events_list** - List Kubernetes events from the must-gather archive with optional filtering by type, namespace, resource, and reason + - `limit` (`integer`) - Maximum number of events to return (default: 100) + - `namespace` (`string`) - Filter by namespace + - `reason` (`string`) - Filter by event reason (partial match) + - `resource` (`string`) - Filter by involved resource name (partial match) + - `type` (`string`) - Event type filter: all, Warning, Normal + +- **mustgather_events_by_resource** - Get all events related to a specific Kubernetes resource from the must-gather archive + - `kind` (`string`) - Resource kind (optional, narrows search) + - `name` (`string`) **(required)** - Resource name + - `namespace` (`string`) - Resource namespace + +- **mustgather_events_by_time** - List Kubernetes events from the must-gather archive within a specific time range, sorted chronologically + - `limit` (`integer`) - Maximum number of events to return (default: 200) + - `namespace` (`string`) - Filter by namespace + - `since` (`string`) **(required)** - Start time in RFC3339 format (e.g. 2026-01-15T10:00:00Z) + - `type` (`string`) - Event type filter: all, Warning, Normal + - `until` (`string`) - End time in RFC3339 format (e.g. 2026-01-15T12:00:00Z) + +- **mustgather_pod_logs_get** - Get container logs for a specific pod from the must-gather archive. Returns current or previous logs. + - `container` (`string`) - Container name (uses first container if not specified) + - `namespace` (`string`) **(required)** - Pod namespace + - `pod` (`string`) **(required)** - Pod name + - `previous` (`boolean`) - Get previous container logs (from crash/restart) + - `tail` (`integer`) - Number of lines from end of logs (0 for all) + +- **mustgather_pod_logs_grep** - Filter pod container logs by a search string. Returns only matching lines from the must-gather archive. + - `caseInsensitive` (`boolean`) - Perform case-insensitive search (default: false) + - `container` (`string`) - Container name (uses first container if not specified) + - `filter` (`string`) **(required)** - String to search for in log lines + - `namespace` (`string`) **(required)** - Pod namespace + - `pod` (`string`) **(required)** - Pod name + - `previous` (`boolean`) - Search previous container logs (from crash/restart) + - `tail` (`integer`) - Maximum number of matching lines to return (0 for all) + +- **mustgather_pod_logs_by_time** - Get pod container logs within a specific time range. Each log line is expected to have an RFC3339Nano timestamp prefix (from kubectl logs --timestamps). + - `container` (`string`) - Container name (uses first container if not specified) + - `limit` (`integer`) - Maximum number of lines to return (default: 500) + - `namespace` (`string`) **(required)** - Pod namespace + - `pod` (`string`) **(required)** - Pod name + - `previous` (`boolean`) - Search previous container logs (from crash/restart) + - `since` (`string`) **(required)** - Start time in RFC3339 format (e.g. 2026-01-15T10:00:00Z) + - `until` (`string`) - End time in RFC3339 format (e.g. 2026-01-15T12:00:00Z) + +- **mustgather_node_diagnostics_get** - Get comprehensive diagnostic information for a specific node including kubelet logs, system info, CPU/IRQ affinities, and hardware details + - `include` (`string`) - Comma-separated diagnostics to include: kubelet,sysinfo,cpu,irq,pods,podresources,lscpu,lspci,dmesg,cmdline (default: all) + - `kubeletTail` (`integer`) - Number of lines from end of kubelet log (0 for all, default: 100) + - `node` (`string`) **(required)** - Node name + +- **mustgather_node_kubelet_logs** - Get kubelet logs for a specific node (decompressed from .gz file) + - `node` (`string`) **(required)** - Node name + - `tail` (`integer`) - Number of lines from end (0 for all) + +- **mustgather_node_kubelet_logs_grep** - Filter kubelet logs for a specific node by a search string. Returns only matching lines. + - `caseInsensitive` (`boolean`) - Perform case-insensitive search (default: false) + - `filter` (`string`) **(required)** - String to search for in log lines + - `node` (`string`) **(required)** - Node name + - `tail` (`integer`) - Maximum number of matching lines to return (0 for all) + +- **mustgather_etcd_health** - Get ETCD cluster health status including endpoint health and active alarms from the must-gather archive + +- **mustgather_etcd_object_count** - Get ETCD object counts by resource type from the must-gather archive + - `limit` (`integer`) - Maximum number of resource types to show (default: 50, sorted by count descending) + +- **mustgather_monitoring_prometheus_status** - Get Prometheus TSDB and runtime status from the must-gather archive + - `replica` (`string`) - Prometheus replica (0, 1, or all). Default: all + +- **mustgather_monitoring_prometheus_targets** - Get Prometheus scrape targets and their health status from the must-gather archive + - `health` (`string`) - Filter by health status: up, down, unknown (default: all) + - `replica` (`string`) - Prometheus replica (0, 1, or all). Default: 0 + +- **mustgather_monitoring_prometheus_tsdb** - Get detailed Prometheus TSDB statistics including top metrics by series count and label cardinality + - `limit` (`integer`) - Number of top entries to show per category (default: 10) + - `replica` (`string`) - Prometheus replica (0, 1, or all). Default: 0 + +- **mustgather_monitoring_prometheus_alerts** - Get active Prometheus alerts from the must-gather archive + - `state` (`string`) - Filter by alert state: firing, pending (default: all) + +- **mustgather_monitoring_prometheus_rules** - Get Prometheus alerting and recording rules from the must-gather archive + - `type` (`string`) - Filter by rule type: alerting, recording (default: all) + +
+ +
+ +ossm + +- **ossm_get_mesh_traffic_graph** - Returns service-to-service traffic topology, dependencies, and network metrics (throughput, response time, mTLS) for the specified namespaces. Use this to diagnose routing issues, latency, or find upstream/downstream dependencies. + - `graphType` (`string`) - Granularity of the graph. 'app' aggregates by app name, 'versionedApp' separates by versions, 'workload' maps specific pods/deployments. Default: versionedApp. + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespaces` (`string`) **(required)** - Comma-separated list of namespaces to map + +- **ossm_get_mesh_status** - Retrieves the high-level health, topology, and environment details of the Istio service mesh. Returns multi-cluster control plane status (istiod), data plane namespace health (including ambient mesh status), observability stack health (Prometheus, Grafana...), and component connectivity. Use this tool as the first step to diagnose mesh-wide issues, verify Istio/Kiali versions, or check overall health before drilling into specific workloads. + +- **ossm_manage_istio_config_read** - Read Istio, Gateway API, and Inference API config. 'list' groups by namespace→'group/version/kind'→{valid:[...],invalid:[...]} where valid/invalid arrays contain resource names; omit group/kind to retrieve ALL config types in a single call. Supports Istio (networking.istio.io, security.istio.io), Gateway API (gateway.networking.k8s.io), and Inference API (inference.networking.k8s.io) when installed. 'get' returns full YAML. For writes use manage_istio_config. + - `action` (`string`) **(required)** - Action to perform (read-only) + - `group` (`string`) - API group of the Istio object. Required ONLY for 'get' action. For 'list', OMIT group and kind to retrieve ALL config types in a single call. Use 'gateway.networking.k8s.io' for Gateway API resources. Use 'inference.networking.k8s.io' for Inference API resources. + - `kind` (`string`) - Kind of the Istio object. Required ONLY for 'get' action. For 'list', OMIT to return all kinds at once — do NOT call separately for each kind. + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespace` (`string`) - Namespace containing the Istio object. For 'list', if not provided, returns objects across all namespaces. For 'get', required. + - `object` (`string`) - Name of the Istio object. Required for 'get' action. + - `serviceName` (`string`) - Filter Istio configurations (VirtualServices, DestinationRules, and their referenced Gateways) that affect a specific service. Only applicable for 'list' action + - `version` (`string`) - API version. Use 'v1' for all resource types. Required for 'get' action. + +- **ossm_manage_istio_config** - Create, patch, or delete Istio, Gateway API, and Inference API config. Supports Istio resources (networking.istio.io, security.istio.io), Gateway API resources (gateway.networking.k8s.io), and Inference API resources (inference.networking.k8s.io) when installed on the cluster. For list and get (read-only) use manage_istio_config_read. + - `action` (`string`) **(required)** - Action to perform (write) + - `data` (`string`) - JSON or YAML data for the resource. Required for create and patch actions. For create, you can provide partial content (e.g. only spec) and it will be merged onto a valid template with defaults. Arrays (like servers, http, etc.) are REPLACED entirely, so include ALL elements you want. + - `group` (`string`) **(required)** - API group of the Istio object. Use 'gateway.networking.k8s.io' for Gateway API resources. Use 'inference.networking.k8s.io' for Inference API resources. + - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'VirtualService', 'DestinationRule'). + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespace` (`string`) **(required)** - Namespace containing the Istio object. + - `object` (`string`) **(required)** - Name of the Istio object. + - `version` (`string`) **(required)** - API version. Use 'v1' for all resource types. + +- **ossm_list_mesh_clusters** - Returns the list of Istio mesh clusters that Kiali can access. Each entry includes its name and whether it is the home cluster (where Kiali is deployed). Call this tool before using meshCluster on other Kiali tools when the target cluster is unknown. + +- **ossm_get_resource_details** - Fetches a list of resources OR retrieves detailed data for a specific resource. If 'resourceName' is omitted, it returns a list. If 'resourceName' is provided, it returns details for that specific resource. + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespaces` (`string`) - Comma-separated list of namespaces to query (e.g., 'bookinfo' or 'bookinfo,default'). If not provided, it will query across all accessible namespaces. + - `resourceName` (`string`) - Optional. The specific name of the resource. If left empty, the tool returns a list of all resources of the specified type. If provided, the tool returns deep details for this specific resource. + - `resourceType` (`string`) **(required)** - The type of resource to query. Use 'app' for Kiali applications (grouped by the Kubernetes 'app' label). Use 'argoapp' for ArgoCD Application CRDs (requires ArgoCD installed and the Kiali service account must have read permissions on applications.argoproj.io). + +- **ossm_list_traces** - Lists distributed traces for a service in a namespace. Returns a summary (namespace, service, total_found, avg_duration_ms) and a list of traces with id, duration_ms, spans_count, root_op, slowest_service, has_errors. Use get_trace_details with a trace id to get full hierarchy. + - `errorOnly` (`boolean`) - If true, only consider traces that contain errors. Default false. + - `limit` (`integer`) - Maximum number of traces to return. Default 10. + - `lookbackSeconds` (`integer`) - How far back to search. Default 600 (10m). + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespace` (`string`) **(required)** - Kubernetes namespace of the service. + - `serviceName` (`string`) **(required)** - Service name to search traces for (required). Returns multiple traces up to limit. + +- **ossm_get_trace_details** - Fetches a single distributed trace by trace_id and returns its call hierarchy (service tree with duration, status, and nested calls). Use this after list_traces to drill into a specific trace. + - `traceId` (`string`) **(required)** - Trace ID to fetch and summarize. If provided, namespace/service_name are ignored. + +- **ossm_get_pod_performance** - Returns a human-readable text summary with current Pod CPU/memory usage (from Prometheus) compared to Kubernetes requests/limits (from the Pod spec). Useful to answer questions like 'Is this workload using too much memory?' + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespace` (`string`) **(required)** - Kubernetes namespace of the Pod. + - `podName` (`string`) - Kubernetes Pod name. If workloadName is provided, the tool will attempt to resolve a Pod from that workload first. + - `queryTime` (`string`) - Optional end timestamp (RFC3339) for the query. Defaults to now. + - `timeRange` (`string`) - Time window used to compute CPU rate (Prometheus duration like '5m', '10m', '1h', '1d'). Defaults to '10m'. + - `workloadName` (`string`) - Kubernetes Workload name (e.g. Deployment/StatefulSet/etc). Tool will look up the workload and pick one of its Pods. If not found, it will fall back to treating this value as a podName. + +- **ossm_get_logs** - Get the logs of a Kubernetes Pod (or workload name that will be resolved to a pod) in a namespace. Output is plain text, matching kubernetes-mcp-server pods_log. The line_count field tells you the total number of log lines returned. Analyze ALL of them, but summarize the results unless the user explicitly asks for the raw output. Do not omit any error or warning lines. + - `container` (`string`) - Optional. Name of the Pod container to get the logs from. + - `format` (`string`) - Output formatting for chat. 'codeblock' wraps logs in ~~~ fences (recommended). 'plain' returns raw text like kubernetes-mcp-server pods_log. + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `name` (`string`) **(required)** - Name of the Pod to get the logs from. If it does not exist, it will be treated as a workload name and a running pod will be selected. + - `namespace` (`string`) **(required)** - Namespace to get the Pod logs from + - `previous` (`boolean`) - Optional. Return previous terminated container logs + - `severity` (`string`) - Optional severity filter applied client-side. Accepts 'ERROR', 'WARN' or combinations like 'ERROR,WARN'. + - `tail` (`integer`) - Number of lines to retrieve from the end of the logs (Optional, defaults to 50). Cannot exceed 200 lines. + - `workload` (`string`) - Optional. Workload name override (used when name lookup fails). + +- **ossm_get_metrics** - Returns a compact JSON summary of Istio metrics (latency quantiles, traffic trends, throughput, payload sizes) for the given resource. + - `byLabels` (`string`) - Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional + - `direction` (`string`) - Traffic direction. Optional, defaults to 'outbound' + - `meshCluster` (`string`) - Optional Istio mesh cluster name from ossm_list_mesh_clusters (e.g. west). When omitted, Kiali defaults to its home cluster. + - `namespace` (`string`) **(required)** - Namespace to get metrics from + - `quantiles` (`string`) - Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional + - `rateInterval` (`string`) - Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '10m' + - `reporter` (`string`) - Metrics reporter(s). Comma-separated list of: 'source', 'destination', 'waypoint', or the special value 'both' (no reporter filter). Optional, defaults to 'source'. Example: 'source,waypoint' + - `requestProtocol` (`string`) - Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional + - `resourceName` (`string`) **(required)** - Name of the resource to get metrics for + - `resourceType` (`string`) **(required)** - Type of resource to get metrics + - `step` (`string`) - Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds + +
+ +
+ +ovn-kubernetes + +- **ovn_show** - Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database. + +For Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers, +their ports, and connections between them. + +For Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings, +and their relationships. Returns 100 lines by default; use head/tail to adjust. + +Example output for nbdb: +{ + "database": "nbdb", + "output": "switch 1234-5678 (node1)\n port node1-k8s\n addresses: [\"00:00:00:00:00:01\"]\n..." +} + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `database` (`string`) **(required)** - OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the pod running OVN (e.g., "ovnkube-node-xxxxx") + - `namespace` (`string`) - Kubernetes namespace of the OVN pod (e.g., "openshift-ovn-kubernetes") + - `tail` (`integer`) - Return only last N lines + +- **ovn_get** - Query records from an OVN database table with flexible filtering. + +This is a versatile command that can: +1. List all records in a table (when no record specified) +2. Get a specific record (when record specified) + +Common Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, +Logical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT + +Common Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow, +MAC_Binding, Multicast_Group, SB_Global + +Returns 100 lines by default; use head/tail to adjust. + +Example listing all records: +{ + "database": "nbdb", + "table": "Port_Group", + "output": "_uuid: 1234-5678\nname: \"pg_default\"\nports: [...]\n\n_uuid: abcd-efgh\n..." +} + +Example getting a specific record: +{ + "database": "nbdb", + "table": "Logical_Router", + "record": "ovn_cluster_router", + "output": "_uuid: 4c4a0a35-348c-41cc-8417-53a618e0c383\nname: ovn_cluster_router\nports: [...]" +} + +Example getting specific columns: +{ + "database": "nbdb", + "table": "Logical_Switch", + "columns": "name,ports", + "output": "name: ovn-worker\nports: [uuid1, uuid2]\n\nname: join\nports: [uuid3]" +} + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `columns` (`string`) - Comma-separated list of columns to display (e.g., "name,_uuid,ports") + - `database` (`string`) **(required)** - OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the pod running OVN + - `namespace` (`string`) - Kubernetes namespace of the OVN pod + - `pattern` (`string`) - Regex pattern to filter results. Only applies when listing all records. + - `record` (`string`) - Record identifier (UUID or name). If not specified, lists all records + - `table` (`string`) **(required)** - Name of the table (e.g., "Logical_Switch", "Port_Binding") + - `tail` (`integer`) - Return only last N lines + +- **ovn_lflow_list** - List logical flows from the OVN Southbound database. + +Runs 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled +logical network pipeline. This is essential for debugging packet forwarding. +Returns 100 lines by default; use head/tail to adjust. + +Example output: +{ + "datapath": "node1", + "flows": [ + "table=0 (ls_in_port_sec_l2), priority=100, match=(inport == \"pod1\"), action=(next;)", + "table=1 (ls_in_port_sec_ip), priority=90, match=(ip4), action=(next;)" + ] +} + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `datapath` (`string`) - Datapath name or UUID to filter flows for a specific logical switch/router + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the pod running OVN + - `namespace` (`string`) - Kubernetes namespace of the OVN pod + - `pattern` (`string`) - Regex pattern to filter flows + - `tail` (`integer`) - Return only last N lines + +- **ovn_trace** - Trace a packet through the OVN logical network. + +Runs 'ovn-trace' to simulate packet processing through the logical network pipeline. +This shows which logical flows match, what actions are taken, and the final disposition. + +The trace is essential for debugging connectivity issues and understanding how traffic +flows through the OVN logical network. Returns 100 lines by default; use head/tail to adjust. + +Microflow specification examples: +- inport=="pod1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5 +- inport=="pod1" && eth.src==00:00:00:00:00:01 && icmp && ip4.src==10.244.0.5 && ip4.dst==8.8.8.8 + +Example output: +{ + "datapath": "node1", + "microflow": "inport==\"pod1\" && ...", + "output": "ingress(dp=\"node1\", inport=\"pod1\")\n 0. ls_in_port_sec_l2: inport == \"pod1\", priority 50, uuid 1234\n next;\n..." +} + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `datapath` (`string`) **(required)** - Name of the logical switch or router to start the trace + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `microflow` (`string`) **(required)** - Microflow specification describing the packet (e.g., "inport==\"pod1\" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5") + - `mode` (`string`) - Output verbosity mode - "detailed" (default), "summary", or "minimal" + - `name` (`string`) **(required)** - Name of the pod running OVN + - `namespace` (`string`) - Kubernetes namespace of the OVN pod + - `pattern` (`string`) - Regex pattern to filter trace output + - `tail` (`integer`) - Return only last N lines + +- **ovs_vsctl** - Run an ovs-vsctl command against an ovnkube-node pod. + +The 'action' parameter selects the ovs-vsctl subcommand to run. + +--- action: "show" --- +Display a comprehensive overview of OVS configuration. + +Runs 'ovs-vsctl show' command and returns detailed information about bridges, ports, interfaces, +controllers, and their configurations in a hierarchical format. + +This command is useful for getting a complete view of the OVS switch configuration including: +- All bridges and their configurations +- Ports and interfaces attached to each bridge +- Controller connections and status +- Interface types and options +- Port configurations and tags + +Example output: +{ + "output": "a1b2c3d4-5678-90ab-cdef-1234567890ab\n Bridge br-int\n Port ovn-k8s-mp0\n Interface ovn-k8s-mp0\n type: internal\n Port br-int\n Interface br-int\n type: internal\n ovs_version: \"2.17.0\"" +} + +--- action: "list-br" --- +List all OVS bridges on a specific pod. + +Runs 'ovs-vsctl list-br' command and returns the names of all configured bridges. + +Example output: +{ + "bridges": [ + "br-int", + "br-ex", + "br-local" + ] +} + +--- action: "list-ports" --- +List all ports on a specific OVS bridge. + +Runs 'ovs-vsctl list-ports' command and returns the names of all ports attached to the specified bridge. + +Example output: +{ + "ports": [ + "patch-br-int-to-br-ex", + "veth1234", + "ovn-k8s-mp0" + ] +} + +--- action: "list-ifaces" --- +List all interfaces on a specific OVS bridge. + +Runs 'ovs-vsctl list-ifaces' command and returns the names of all interfaces attached to the specified bridge. + +Example output: +{ + "interfaces": [ + "patch-br-int-to-br-ex", + "veth1234", + "ovn-k8s-mp0" + ] +} + - `action` (`string`) **(required)** - The ovs-vsctl subcommand to run: "show", "list-br", "list-ports", or "list-ifaces" + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head (only used when action is "show"). Default: false + - `bridge` (`string`) - Name of the OVS bridge (required for "list-ports" and "list-ifaces"; e.g., "br-int") + - `head` (`integer`) - Return only first N lines (only used when action is "show"). Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the ovnkube-node pod (e.g., "ovnkube-node-xxxxx") + - `namespace` (`string`) **(required)** - Kubernetes namespace of the ovnkube-node pod (e.g., "openshift-ovn-kubernetes") + - `tail` (`integer`) - Return only last N lines (only used when action is "show") + +- **ovs_ofctl** - Run an ovs-ofctl command against an ovnkube-node pod. + +The 'action' parameter selects the ovs-ofctl subcommand to run. + +--- action: "dump-flows" --- +Dump OpenFlow flows from a specific OVS bridge. + +Runs 'ovs-ofctl dump-flows' command on the specified bridge and returns the flow entries. + +Example output: +{ + "bridge": "br-int", + "flows": [ + "cookie=0x0, duration=123.456s, table=0, n_packets=100, n_bytes=10000, priority=100,in_port=1 actions=output:2", + "cookie=0x0, duration=123.456s, table=0, n_packets=50, n_bytes=5000, priority=90,in_port=2 actions=output:1" + ] +} + - `action` (`string`) **(required)** - The ovs-ofctl subcommand to run: "dump-flows" + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `bridge` (`string`) **(required)** - Name of the OVS bridge (e.g., "br-int") + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the ovnkube-node pod (e.g., "ovnkube-node-xxxxx") + - `namespace` (`string`) **(required)** - Kubernetes namespace of the ovnkube-node pod (e.g., "openshift-ovn-kubernetes") + - `pattern` (`string`) - Regex pattern to filter output lines + - `tail` (`integer`) - Return only last N lines + +- **ovs_appctl** - Run an ovs-appctl command against an ovnkube-node pod. + +The 'action' parameter selects the ovs-appctl subcommand to run. + +--- action: "dpctl/dump-conntrack" --- +Dump connection tracking entries from OVS datapath. + +Runs 'ovs-appctl dpctl/dump-conntrack' command and returns the conntrack entries. + +Connection tracking (conntrack) maintains state for stateful firewall rules and NAT. +Each entry shows source/destination IPs, ports, protocol, connection state, and more. + +Example output: +{ + "entries": [ + "tcp,orig=(src=10.244.0.5,dst=10.96.0.1,sport=45678,dport=443),reply=(src=10.96.0.1,dst=10.244.0.5,sport=443,dport=45678)", + "udp,orig=(src=10.244.0.3,dst=8.8.8.8,sport=53214,dport=53),reply=(src=8.8.8.8,dst=10.244.0.3,sport=53,dport=53214)" + ] +} + +--- action: "ofproto/trace" --- +Trace a packet through the OpenFlow pipeline. + +Runs 'ovs-appctl ofproto/trace' command to simulate packet processing through OpenFlow tables. +This shows which flows match, what actions are taken, and the final disposition of the packet. + +The trace output is essential for debugging flow rules, understanding packet forwarding decisions, +and troubleshooting connectivity issues. + +Flow specification examples: +- "in_port=1,icmp" +- "in_port=2,ip,nw_src=192.168.1.10,nw_dst=192.168.1.20" +- "in_port=3,tcp,nw_src=10.0.0.1,nw_dst=10.0.0.2,tp_src=12345,tp_dst=80" + +Example output: +{ + "bridge": "br-int", + "flow": "in_port=1,ip,nw_src=10.244.0.5,nw_dst=10.96.0.1", + "output": "Flow: ip,in_port=1,nw_src=10.244.0.5,nw_dst=10.96.0.1\n\nbridge(\"br-int\")\n-------------\n 0. priority 100\n resubmit(,10)\n10. ip,nw_dst=10.96.0.1, priority 200\n load:0x1->NXM_NX_REG0[]\n resubmit(,20)\n...\nFinal flow: ...\nDatapath actions: ..." +} + - `action` (`string`) **(required)** - The ovs-appctl subcommand to run: "dpctl/dump-conntrack" or "ofproto/trace" + - `additional_params` (`array`) - Additional CLI arguments (only used when action is "dpctl/dump-conntrack"; e.g., ["zone=5"]) + - `apply_tail_first` (`boolean`) - If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false + - `bridge` (`string`) - Name of the OVS bridge (required for "ofproto/trace"; e.g., "br-int") + - `flow` (`string`) - Flow specification (required for "ofproto/trace"; e.g., "in_port=1,ip,nw_src=10.244.0.5,nw_dst=10.96.0.1") + - `head` (`integer`) - Return only first N lines. Default: 100 lines if tail is not specified + - `name` (`string`) **(required)** - Name of the ovnkube-node pod (e.g., "ovnkube-node-xxxxx") + - `namespace` (`string`) **(required)** - Kubernetes namespace of the ovnkube-node pod (e.g., "openshift-ovn-kubernetes") + - `pattern` (`string`) - Regex pattern to filter output lines + - `tail` (`integer`) - Return only last N lines + +
+ +
+ tekton - **tekton_pipeline_start** - Start a Tekton Pipeline by creating a PipelineRun that references it @@ -1273,22 +1831,6 @@ Use tempo_search_tags to discover available tag names.
-kubevirt - -- **vm-troubleshoot** - Generate a step-by-step troubleshooting guide for diagnosing KubeVirt VirtualMachine issues - - `namespace` (`string`) **(required)** - The namespace of the VirtualMachine to troubleshoot - - `name` (`string`) **(required)** - The name of the VirtualMachine to troubleshoot - -- **windows-golden-image** - Guides creation of a Windows golden image via the KubeVirt windows-efi-installer Tekton pipeline - - `winImageDownloadURL` (`string`) **(required)** - Microsoft Windows ISO download URL (must be https://) - - `namespace` (`string`) - Target namespace for the PipelineRun - - `windowsVersion` (`string`) - Windows version: 10, 11, 2k22 (default), or 2k25 - - `pipelineVersion` (`string`) - Pipeline version (default: latest). Use specific version like 0.25.0 if needed - -
- -
- tekton - **pipeline-troubleshoot** - Gather PipelineRun status, TaskRuns, logs, events, Pipeline-as-Code Repository, and TektonConfig context for Tekton troubleshooting @@ -1304,6 +1846,30 @@ Use tempo_search_tags to discover available tag names. +
+ +openshift/mustgather + +- **must-gather** - Loaded must-gather archive metadata + - URI: `must-gather://current` + - MIME Type: `text/plain` +- **must-gather-namespaces** - List of all namespaces in the must-gather archive + - URI: `must-gather://current/namespaces` + - MIME Type: `text/plain` +- **must-gather-etcd-members** - ETCD cluster member list from the must-gather archive + - URI: `must-gather://current/etcd/members` + - MIME Type: `application/json` +- **must-gather-etcd-endpoint-status** - ETCD endpoint status from the must-gather archive + - URI: `must-gather://current/etcd/endpoint-status` + - MIME Type: `application/json` +- **must-gather-prometheus-config** - Prometheus configuration summary from the must-gather archive + - URI: `must-gather://current/prometheus/config` + - MIME Type: `text/plain` +- **must-gather-alertmanager-status** - AlertManager status from the must-gather archive + - URI: `must-gather://current/alertmanager/status` + - MIME Type: `text/plain` +
+ @@ -1311,6 +1877,15 @@ Use tempo_search_tags to discover available tag names. +
+ +openshift/mustgather + +- **must-gather-resource** - A specific Kubernetes resource from the must-gather archive as YAML. Use '-' for empty group (core API) or cluster-scoped namespace. + - URI Template: `must-gather://current/resources/{group}/{version}/{kind}/{namespace}/{name}` + - MIME Type: `text/yaml` +
+ diff --git a/docs/configuration.md b/docs/configuration.md index 493058b82..f292e35ac 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -349,14 +349,25 @@ Toolsets group related tools together. Enable only the toolsets you need to redu | Toolset | Description | Default | |-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | -| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | -| helm | Tools for managing Helm charts and releases | | -| kcp | Manage kcp workspaces and multi-tenancy features | | -| kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | | -| kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | | -| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | | -| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, TaskRuns, and troubleshooting. | | +| cluster-diagnostics | Tools for cluster diagnostics and troubleshooting | | +| cni-diagnostics | Tools for Container Network Interface (CNI) diagnostics and troubleshooting | | +| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | +| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | +| helm | Tools for managing Helm charts and releases | | +| kcp | Manage kcp workspaces and multi-tenancy features | | +| kubevirt | OpenShift Virtualization tools for managing virtual machines, check the [OpenShift Virtualization documentation](https://github.com/openshift/openshift-mcp-server/blob/main/docs/kubevirt.md) for more details. | | +| netedge | NetEdge troubleshooting tools for OpenShift | | +| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | | +| oadp | OADP (OpenShift API for Data Protection) tools for managing Velero backups, restores, and schedules | | +| observability/logs | Toolset for querying Loki logs | | +| observability/metrics | Toolset for querying Prometheus and Alertmanager endpoints in efficient ways. | | +| observability/otelcol | Toolset for OpenTelemetry Collector configuration assistance including schema validation, component documentation, and version management. | | +| observability/traces | Distributed tracing tools for discovering Tempo instances, searching and retrieving traces, and exploring trace attributes. | | +| openshift | OpenShift-specific tools for cluster management and troubleshooting | | +| openshift/mustgather | Analyze OpenShift must-gather archives offline without a live cluster connection | | +| ossm | Most common tools for managing OSSM, check the [OSSM documentation](https://github.com/openshift/openshift-mcp-server/blob/main/docs/OSSM.md) for more details. | | +| ovn-kubernetes | OVN-Kubernetes CNI network troubleshooting tools | | +| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, TaskRuns, and troubleshooting. | | @@ -370,6 +381,30 @@ toolsets = ["core", "config", "helm", "kubevirt"] +
+ +openshift/mustgather + +- **must-gather** - Loaded must-gather archive metadata + - URI: `must-gather://current` + - MIME Type: `text/plain` +- **must-gather-namespaces** - List of all namespaces in the must-gather archive + - URI: `must-gather://current/namespaces` + - MIME Type: `text/plain` +- **must-gather-etcd-members** - ETCD cluster member list from the must-gather archive + - URI: `must-gather://current/etcd/members` + - MIME Type: `application/json` +- **must-gather-etcd-endpoint-status** - ETCD endpoint status from the must-gather archive + - URI: `must-gather://current/etcd/endpoint-status` + - MIME Type: `application/json` +- **must-gather-prometheus-config** - Prometheus configuration summary from the must-gather archive + - URI: `must-gather://current/prometheus/config` + - MIME Type: `text/plain` +- **must-gather-alertmanager-status** - AlertManager status from the must-gather archive + - URI: `must-gather://current/alertmanager/status` + - MIME Type: `text/plain` +
+ @@ -377,6 +412,15 @@ toolsets = ["core", "config", "helm", "kubevirt"] +
+ +openshift/mustgather + +- **must-gather-resource** - A specific Kubernetes resource from the must-gather archive as YAML. Use '-' for empty group (core API) or cluster-scoped namespace. + - URI Template: `must-gather://current/resources/{group}/{version}/{kind}/{namespace}/{name}` + - MIME Type: `text/yaml` +
+