Skip to content

Conversation

@arkadeepsen
Copy link

@arkadeepsen arkadeepsen commented Sep 17, 2025

This PR adds the kubernetes layer of the ovnk mcp server.
Tools exposed:

  • pod-logs: Fetches the logs of a container in a pod.
  • resource-get: Fetches a resource from kube api-server. Group, version and kind is needed from fetching the specific resource.
  • resource-list: Lists resources from kube api-server. Group, version and kind is needed from listing the resources of this kind.

In addition to the above tools, pod exec and node debug is also supported as internal functions. These functions can be used by other layers of the MCP server for running necessary commands.

This PR also sets up the main entrypoint of the mcp server where the individual layers can be added to the main server.

Fixes: #4

@arkadeepsen arkadeepsen force-pushed the k8s-layer branch 20 times, most recently from 8f4c440 to 4f1e222 Compare September 19, 2025 10:00
@arkadeepsen arkadeepsen changed the title Add kubernetes mcp server for ovnk debugging Add kubernetes layer for the ovnk mcp server Sep 19, 2025
{
"mcpServers": {
"ovn-kubernetes": {
"command": "/PATH-TO-THE-LOCAL-GIT-REPO/_output/ovnk-mcp-server",
Copy link

@Meina-rh Meina-rh Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe consider npx command so we don’t need to build and reference _output directly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check this. As the code is written in Go, I'll have to check how npx command can be used.

Copy link
Author

@arkadeepsen arkadeepsen Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made some changes in the directory structure. Once this PR merges we can use the following format:

{
  "mcpServers": {
    "ovn-kubernetes": {
      "command": "go",
      "args": [
        "run",
        "github.com/ovn-kubernetes/ovn-kubernetes-mcp/cmd/ovnk-mcp-server@latest",
        "--kubeconfig",
        "/PATH-TO-THE-KUBECONFIG-FILE"
      ]
    }
  }
}

Comment on lines +13 to +44
gvk := schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which objects we must provide in different tools? for example, I test resource-get, it hints

MCP error -32602: invalid params: validating "arguments": validating root: required: missing properties: ["group" "kind" "name"]

when I test resource-list, it hints

MCP error -32602: invalid params: validating "arguments": validating root: required: missing properties: ["version" "kind"]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which objects we must provide in different tools? for example, I test resource-get, it hints

MCP error -32602: invalid params: validating "arguments": validating root: required: missing properties: ["group" "kind" "name"]

The GetResourceParams define the fields for the resource-get tool. The fields which are not marked as omitempty are required fields.

when I test resource-list, it hints

MCP error -32602: invalid params: validating "arguments": validating root: required: missing properties: ["version" "kind"]

The ListResourceParams gives the details of the fields for the resource-list tool.

)

// GetResourceGVK gets a resource by group, version, kind, name and namespace.
func (client *OVNKMCPServerClientSet) GetResourceGVK(group, version, kind, resourceName, namespace string) (*unstructured.Unstructured, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I test to get CUDN resource using this tool, the output is

{
  "resource": {
    "age": "13s",
    "data": "",
    "name": "l2-network-cudn1"
  }
}

should also print the status of resources?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the resource type is generic, it's difficult to provide status for all types of objects as it may be different for different types. For getting more details, you can use json/yaml as outputType value, which will provide the full details of the object.

@arkadeepsen
Copy link
Author

Tagging @arghosh93 for +1 review. Arnab, heads up, the PR is not ready for review. There might be some changes brought in to the PR. Will let you know when it's ready for review.

@tssurya tssurya requested a review from arghosh93 September 23, 2025 12:13
@arkadeepsen arkadeepsen force-pushed the k8s-layer branch 4 times, most recently from f71ac59 to e4499af Compare September 29, 2025 14:31
@@ -0,0 +1,56 @@
package client

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general pattern in K8s is to put fake implementations in a separate package named fake.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields of the OVNKMCPServerClientSet struct are unexported, thus creating a separate fake package won't work here.

"k8s.io/pod-security-admission/api"
)

func (client *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string, isOpenshift bool) (string, string, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep receiver names short - https://go.dev/wiki/CodeReviewComments#receiver-names

Suggested change
func (client *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string, isOpenshift bool) (string, string, error) {
func (c *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string, isOpenshift bool) (string, string, error) {


cleanupNamespace := func() {
// Delete the namespace.
client.clientSet.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should at least log the returned error as warning instead of silencing it.

}
ns, err := client.clientSet.CoreV1().Namespaces().Create(ctx, tmpNS, metav1.CreateOptions{})
if err != nil {
return "", nil, err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add context to the error. Also in other places. In general, it's good practice to Wrap errors returned from other packages.

Suggested change
return "", nil, err
return "", nil, errors.Wrap(err, "failed to create temporary namespace")

Comment on lines 114 to 115
Privileged: &isTrue,
RunAsUser: &zero,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import "k8s.io/utils/ptr"

Suggested change
Privileged: &isTrue,
RunAsUser: &zero,
Privileged: ptr.To(true),
RunAsUser: ptr.To(int64(0)),

"k8s.io/client-go/tools/remotecommand"
)

func TestGetPodLogs(t *testing.T) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should prefer to use the Ginkgo framework for unit tests rather than the legacy Go testing.

Copy link
Author

@arkadeepsen arkadeepsen Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used Ginkgo framework in the e2e tests. As the unit tests are pretty straightforward I haven't used the Ginkgo framework.

}

// GetFormattedJSONData gets the JSON data from a resource.
func (j *FormattedOutput) GetFormattedJSONData(data any) error {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (j *FormattedOutput) GetFormattedJSONData(data any) error {
func (j *FormattedOutput) ToJSON(data any) error {

}

// GetFormattedYAMLData gets the YAML data from a resource.
func (j *FormattedOutput) GetFormattedYAMLData(data any) error {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (j *FormattedOutput) GetFormattedYAMLData(data any) error {
func (j *FormattedOutput) ToYAML(data any) error {

err = errors.New("version is required")
}
if in.Kind == "" {
errors.Join(err, errors.New("kind is required"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return error is ignored so this doesn't actually do anything. With errors.Join, typically you build an errs slice:

var errs []error

errs = append(errs, ....)

err := errors.Join(errs...)

)

// FormatAge formats the age of a resource in a human readable format.
func FormatAge(age time.Duration) string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use time.Duration.String()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time.Duration.String() only formats till hour but not beyond that. Here I have added days formatting as well which is also supported by kubectl.

import "strings"

// checkOpenshift checks if the cluster is an Openshift cluster.
func (s *OVNKMCPServerClientSet) CheckOpenshift() bool {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you referencing openshift here? We should not have any ocp specific code as part of this repository.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that oc debug would need additional openshift specific configs such as this one: https://github.com/openshift/oc/blob/a25e854ffddffcf2f8c1bc99b10afd1b34b8e0ca/pkg/cli/debug/debug.go#L1086.
However, I tested the code without the referencing to openshift and it worked without any issues.

I'll remove the references to openshift.

@arkadeepsen arkadeepsen force-pushed the k8s-layer branch 3 times, most recently from 4e4999c to 7a9a82f Compare October 10, 2025 06:12
Mode string
Transport string
Port string
Kubernetes kubernetes.Config

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to re run this to connect to different k8 cluster?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is currently designed to connect to a single cluster.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be very interested in support for multiple clusters. I can take it up but think it would be better to design now with that in mind.

switch serverCfg.Transport {
case "stdio":
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
if err := ovnkMcpServer.Run(context.Background(), t); err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be create context.cancel().
so we can terminate the server when we close the main function

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using a separate goroutine to run the server, so when the main function exits, it also terminates the server as well.

Copy link

@FrostGod FrostGod Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience, it's preferable to use a single context and pass it to the routine. This approach simplifies future modifications—if we need to add functionality later, we can simply pass the same context and invoke cancel() to terminate all related operations simultaneously.

"k8s.io/utils/ptr"
)

func (c *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string) (string, string, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not work,
say multiple people are connected to same k8s-cluster, for some reason they end up running this tool.
and we might result in a conflict failure with same pod name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pod name has a random string attached to it at the end to avoid this issue. It works the same way how kubectl debug node works.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my bad, yes this is not a static config or a stateful set

Operator: corev1.TolerationOpExists,
},
},
HostNetwork: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can be a overlay pod too right ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For node debug we need to run the pod as host networked otherwise we'd not be able to run the commands at the node level. For reference, kubectl code: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/debug/profiles.go#L344-L350

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have this as an open thing
overlay pod or hostnetwork pod,
there are somethings which you can see as an overlay pod and some as hostnetwork pod.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this only for running the tools like ip, route, etc. at a specific node. For running CLI commands on a specific pod, the ExecPod function can be used. From the usecase of ovnk debugging I am not sure we'd need to create a non-host networked pod.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can take this in another MR?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OKEP (ovn-kubernetes/ovn-kubernetes#5496) only mentions kubectl debug node. If we want to add support for kubectl debug pod, we'll have to update the OKEP and add the reason behind adding the support for it. Once that's accepted, then we can take it up in a follow-up work.


// GetPodLogs gets the logs of a pod by name and namespace.
func (c *OVNKMCPServerClientSet) GetPodLogs(ctx context.Context, namespace string, name string, container string) ([]string, error) {
req := c.clientSet.CoreV1().Pods(namespace).GetLogs(name,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - sometimes the pod we are debugging might be restarting multiple times, so better to get the --previous logs then.
2 - caution - logs might be huge

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add an option in the tools to mention whether or not to fetch previous log.

The logs being huge is definitely a concern, but we plan to address that in the future. Current, focus is to quickly get the tools out for the phase 1 implementation.

Copy link

@huiran0826 huiran0826 Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arkadeepsen Regarding pod-logs resource, what is the required parameters? Based on current output, it seems only 'name' is required.

Failed to call tool pod-logs: MCP error -32602: invalid params: validating "arguments": validating root: required: missing properties: ["name"]

However with a name provided command, it will fail with below error, do we need namespace as required as well for pod-logs or use default other than empty namespace?

{
  "content": [
    {
      "type": "text",
      "text": "failed to fetch pod logs: an empty namespace may not be set when a resource name is provided"
    }
  ],
  "isError": true

cc @Meina-rh

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huiran0826 I have added some example inputs for each of the tools in the Description of each tool: https://github.com/ovn-kubernetes/ovn-kubernetes-mcp/pull/1/files#diff-5822552c214b7ad8b61a70604fb330f43d2178756aae795d33677922357dde06R45-R66

You can also find the details of each of the tools from the handler function which is being added through the AddTools function.

Hope that helps in understanding the input fields.


// Cannot exec into a container in a completed pod.
if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
return "", "", fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print the command as well

}

// ExecPod executes a command in a pod by name and namespace.
func (c *OVNKMCPServerClientSet) ExecPod(ctx context.Context, name, namespace, container string, command []string) (string, string, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this be executing only in the debug-pod ? or any generic pod

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for executing commands in the existing pods running in the cluster. Not a separate debug pod.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should give this permission to LLM to run exec command in any running pod in my opinion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not giving direct exec access to the LLM models. This tool is not exposed. It'll be utilized by the other layers to run the different CLI commands related to ovn, ovs, etc. Additionally, only read-only commands necessary for debugging will only be supported.

}

// GetResourceGVK gets a resource by group, version, kind, name and namespace.
func (c *OVNKMCPServerClientSet) GetResourceGVK(group, version, kind, resourceName, namespace string) (*unstructured.Unstructured, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might consider renaming it
GetDynamicResource or GetResource, or any other name

getresourceGVK is not very intutitive
similarly GVR

Name: "pod-logs",
Description: "Get the logs of a pod",
}, s.GetPodLogs)
mcp.AddTool(server,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://modelcontextprotocol.io/docs/learn/server-concepts#resources

I don't think all of them are tools, some of them might be resources,
for example get logs should be a resource
please cross-check though

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I understood is that resources have fixed URIs.

Let's keep this as MCP tool as the logs are not being fetched using fixed URIs.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this into pkg/mcp
all these file pods,nodes,resources,mcp.go

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it into pkg/kubernetes/mcp. There are going to be other layers like ovn, ovs, etc. each having their own set of tools and server implementation. Thus, adding these files into pkg/mcp may not be a good idea.

Mode string
Transport string
Port string
Kubernetes kubernetes.Config
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be very interested in support for multiple clusters. I can take it up but think it would be better to design now with that in mind.

"ovn-kubernetes": {
"command": "/PATH-TO-THE-LOCAL-GIT-REPO/_output/ovnk-mcp-server",
"args": [
"--kubeconfig",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be problematic if we want to add multicluster support in future. --kubeconfigs would work better even if we only support single for now. clusterconfigs or clusters might be most preferred.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure @vthapar. I'll update the PR to consume multiple kubeconfigs. However, for now I think I'll make only minimal changes for supporting multicluster. I think it'd be better to take this up as a follow-up work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I have not made any changes to this. Taking multiple kubeconfigs as input but not supporting multicluster didn't seem intuitive for the users. I'll create a separate issue to track the multicluster work which can be taken up as a follow-up work where all the changes related to multicluster can be made.

@FrostGod
Copy link

FrostGod commented Oct 17, 2025

+1, LG to me, If there is any other changes requried lets take in further MRs, so that other people start contributing and can build on top of this initial MR.

Copy link

@kyrtapz kyrtapz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I have some comments that I think need to be addressed I am ok to get this PR in to help move things forward under the condition we will track the things we need to improve in the future.

}

// Execute the command in the pod.
stdout, stderr, err := c.ExecPod(ctx, debugPodName, namespace, "debug-container", command)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we leaving the pod running if the exec command fails?
I think we should run the cleanupPod in defer.

// Create a host networked privileged debug pod.
debugPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debug-node-" + node + "-" + utilrand.String(5),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use GenerateName instead?

return pod.Status.Phase == corev1.PodRunning, nil
})
if err != nil {
return "", nil, fmt.Errorf("debug pod did not reach running state within timeout of 5 minutes: %w", err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the pod never starts shouldn't it be removed?

}

// Cannot exec into a container in a completed pod.
if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not pod.Status.Phase != corev1.PodRunning you are missing Pending,Unknown here anyway

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following how kubectl handles pod exec, but this makes more sense. I have changed it to pod.Status.Phase != corev1.PodRunning.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// If no container is specified, use the first container.
if container == "" {
container = pod.Spec.Containers[0].Name
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will ALWAYS be at least one container in a pod right? we are not checking for length here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the godoc, there should be at least one container in a pod: https://github.com/kubernetes/api/blob/v0.34.1/core/v1/types.go#L4115-L4123

@@ -0,0 +1,39 @@
# Get the Git repository root directory
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have and run a linter ASAP to avoid future cleanup. Doesn't have to be done as part of this PR I guess.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a linter similar to the ovnk repo.

Copy link

@jluhrsen jluhrsen Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha. didn't see this. I created this PR as part of a learning excercise. It has one difference from ovnk. the lint that would run in GH actions would be our make lint which is what devs do locally instead of the marketplace (is that the right term) action for golangci-lint

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great @jluhrsen. I also made a some minor changes in the hack/lint.sh script to make shellcheck happy. Maybe once this PR merges you can rebase your PR to add the GH action. I think you can add the actions for some of the other make targets as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes @arkadeepsen , I will track this one and when it lands I'll update mine and we'll go from there. thanks

}

func (s *MCPServer) AddTools(server *mcp.Server) {
mcp.AddTool(server,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all added tools are missing an InputSchema and maybe some examples in the description. Looking at comments like this makes me think it is important to be usable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added more details in the description with examples for the tools. I have not added the InputSchema and the OutputSchema as that is done by the AddTools function itself. It is being deciphered from the handler function itself.

}

// ExecPod executes a command in a pod by name and namespace.
func (s *MCPServer) ExecPod(ctx context.Context, req *mcp.CallToolRequest, in types.ExecPodParams) (*mcp.CallToolResult, types.ExecPodResult, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used anywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecPod is not being exposed as a tool as this needs write permissions. This tool can be used internally by the other layers like ovn, ovs, etc, for invoking the different CLI commands.

Namespace string `json:"namespace,omitempty"`
LabelSelector string `json:"labelSelector,omitempty"`
// OutputType is the output type of the resource. If set, it can be YAML, JSON or wide.
OutputType OutputType `json:"outputType,omitempty"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all of the different output types? I am especially curious about wide

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have provided the different output types like kubectl. The wide option currently provides the annotations and labels as additional info apart from the default fields. The main challenge to provide additonal info is because we don't know about the type of resource that is being fetched here. Maybe, in future we can infer the resource type and provide additonal info for the known types.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question about the OutputType, this is output when I used 'outputType=json', it seems the content in "data" cannot be parsed further by json, just wondering is this expected? And it may not directly can retrieve specific resource status or field value from this output by json.


{
  "content": [
    {
      "type": "text",
      "text": "{\"resources\":[{\"data\":\"{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"kind\\\":\\\"EgressIP\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"k8s.ovn.org/egressip-mark\\\":\\\"50000\\\"},\\\"creationTimestamp\\\":\\\"2025-10-28T01:24:35Z\\\",\\\"generation\\\":2,\\\"managedFields\\\":[{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:spec\\\":{\\\".\\\":{},\\\"f:egressIPs\\\":{},\\\"f:namespaceSelector\\\":{}}},\\\"manager\\\":\\\"kubectl-create\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-28T01:24:35Z\\\"},{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:metadata\\\":{\\\"f:annotations\\\":{\\\".\\\":{},\\\"f:k8s.ovn.org/egressip-mark\\\":{}}},\\\"f:status\\\":{\\\".\\\":{},\\\"f:items\\\":{}}},\\\"manager\\\":\\\"ovn-control-plane\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-28T01:24:35Z\\\"}],\\\"name\\\":\\\"egressip-1\\\",\\\"resourceVersion\\\":\\\"9148\\\",\\\"uid\\\":\\\"e02aa851-182b-4c7c-a3b2-67e24a424c7b\\\"},\\\"spec\\\":{\\\"egressIPs\\\":[\\\"172.18.0.100\\\"],\\\"namespaceSelector\\\":{\\\"matchLabels\\\":{\\\"name\\\":\\\"test\\\"}}},\\\"status\\\":{\\\"items\\\":[{\\\"egressIP\\\":\\\"172.18.0.100\\\",\\\"node\\\":\\\"ovn-worker2\\\"}]}}\"},{\"data\":\"{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"kind\\\":\\\"EgressIP\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"k8s.ovn.org/egressip-mark\\\":\\\"50001\\\"},\\\"creationTimestamp\\\":\\\"2025-10-28T02:00:34Z\\\",\\\"generation\\\":2,\\\"managedFields\\\":[{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:spec\\\":{\\\".\\\":{},\\\"f:egressIPs\\\":{},\\\"f:namespaceSelector\\\":{}}},\\\"manager\\\":\\\"kubectl-create\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-28T02:00:34Z\\\"},{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:metadata\\\":{\\\"f:annotations\\\":{\\\".\\\":{},\\\"f:k8s.ovn.org/egressip-mark\\\":{}}},\\\"f:status\\\":{\\\".\\\":{},\\\"f:items\\\":{}}},\\\"manager\\\":\\\"ovn-control-plane\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-28T02:00:34Z\\\"}],\\\"name\\\":\\\"egressip-2\\\",\\\"resourceVersion\\\":\\\"12873\\\",\\\"uid\\\":\\\"2c31adee-47f4-4e02-beb3-dc145616fc2a\\\"},\\\"spec\\\":{\\\"egressIPs\\\":[\\\"172.18.0.101\\\"],\\\"namespaceSelector\\\":{\\\"matchLabels\\\":{\\\"name\\\":\\\"test\\\"}}},\\\"status\\\":{\\\"items\\\":[{\\\"egressIP\\\":\\\"172.18.0.101\\\",\\\"node\\\":\\\"ovn-worker2\\\"}]}}\"}]}"
    }
  ],
  "structuredContent": {
    "resources": [
      {
        "data": "{\"apiVersion\":\"k8s.ovn.org/v1\",\"kind\":\"EgressIP\",\"metadata\":{\"annotations\":{\"k8s.ovn.org/egressip-mark\":\"50000\"},\"creationTimestamp\":\"2025-10-28T01:24:35Z\",\"generation\":2,\"managedFields\":[{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:spec\":{\".\":{},\"f:egressIPs\":{},\"f:namespaceSelector\":{}}},\"manager\":\"kubectl-create\",\"operation\":\"Update\",\"time\":\"2025-10-28T01:24:35Z\"},{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:annotations\":{\".\":{},\"f:k8s.ovn.org/egressip-mark\":{}}},\"f:status\":{\".\":{},\"f:items\":{}}},\"manager\":\"ovn-control-plane\",\"operation\":\"Update\",\"time\":\"2025-10-28T01:24:35Z\"}],\"name\":\"egressip-1\",\"resourceVersion\":\"9148\",\"uid\":\"e02aa851-182b-4c7c-a3b2-67e24a424c7b\"},\"spec\":{\"egressIPs\":[\"172.18.0.100\"],\"namespaceSelector\":{\"matchLabels\":{\"name\":\"test\"}}},\"status\":{\"items\":[{\"egressIP\":\"172.18.0.100\",\"node\":\"ovn-worker2\"}]}}"
      },
      {
        "data": "{\"apiVersion\":\"k8s.ovn.org/v1\",\"kind\":\"EgressIP\",\"metadata\":{\"annotations\":{\"k8s.ovn.org/egressip-mark\":\"50001\"},\"creationTimestamp\":\"2025-10-28T02:00:34Z\",\"generation\":2,\"managedFields\":[{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:spec\":{\".\":{},\"f:egressIPs\":{},\"f:namespaceSelector\":{}}},\"manager\":\"kubectl-create\",\"operation\":\"Update\",\"time\":\"2025-10-28T02:00:34Z\"},{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:annotations\":{\".\":{},\"f:k8s.ovn.org/egressip-mark\":{}}},\"f:status\":{\".\":{},\"f:items\":{}}},\"manager\":\"ovn-control-plane\",\"operation\":\"Update\",\"time\":\"2025-10-28T02:00:34Z\"}],\"name\":\"egressip-2\",\"resourceVersion\":\"12873\",\"uid\":\"2c31adee-47f4-4e02-beb3-dc145616fc2a\"},\"spec\":{\"egressIPs\":[\"172.18.0.101\"],\"namespaceSelector\":{\"matchLabels\":{\"name\":\"test\"}}},\"status\":{\"items\":[{\"egressIP\":\"172.18.0.101\",\"node\":\"ovn-worker2\"}]}}"
      }
    ]
  }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Huiran, if you are using MCP inspector to test the PR, you can check out the test/e2e directory where I have created a framework to programmatically use the MCP inspector in the test. For parsing the data returned by it I have written the following function. I think it will help you to get the correct data from the fields: https://github.com/ovn-kubernetes/ovn-kubernetes-mcp/pull/1/files#diff-2547c5a5595d73ed4720c414c96d15d919e53abf7441f91f1b2c7250196a2606R11-R25

Copy link

@huiran0826 huiran0826 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arkadeepsen Thanks, I checked that function, it seems the e2e is example without outputType=json . Actually without 'outputType=json', the structuredContent can be fully parsed by json

% npx @modelcontextprotocol/inspector --cli ./_output/ovnk-mcp-server --kubeconfig=${KUBECONFIG} --method tools/call --tool-name resource-list --tool-arg group=k8s.ovn.org kind=EgressIP version=v1 | jq .
{
  "content": [
    {
      "type": "text",
      "text": "{\"resources\":[{\"age\":\"10m58s\",\"data\":\"\",\"name\":\"egressip-2\"}]}"
    }
  ],
  "structuredContent": {
    "resources": [
      {
        "age": "10m58s",
        "data": "",
        "name": "egressip-2"
      }
    ]
  }
}

However, with 'outputType=json', we can see the data part cannot be parsed by jq. Just curious if this kind of data part can be parsed? I'm sorry if I misunderstand anything or not?

% npx @modelcontextprotocol/inspector --cli ./_output/ovnk-mcp-server --kubeconfig=${KUBECONFIG} --method tools/call --tool-name resource-list --tool-arg group=k8s.ovn.org kind=EgressIP version=v1 outputType=json | jq .
{
  "content": [
    {
      "type": "text",
      "text": "{\"resources\":[{\"data\":\"{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"kind\\\":\\\"EgressIP\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"k8s.ovn.org/egressip-mark\\\":\\\"50000\\\"},\\\"creationTimestamp\\\":\\\"2025-10-30T08:56:56Z\\\",\\\"generation\\\":2,\\\"managedFields\\\":[{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:spec\\\":{\\\".\\\":{},\\\"f:egressIPs\\\":{},\\\"f:namespaceSelector\\\":{}}},\\\"manager\\\":\\\"kubectl-create\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-30T08:56:56Z\\\"},{\\\"apiVersion\\\":\\\"k8s.ovn.org/v1\\\",\\\"fieldsType\\\":\\\"FieldsV1\\\",\\\"fieldsV1\\\":{\\\"f:metadata\\\":{\\\"f:annotations\\\":{\\\".\\\":{},\\\"f:k8s.ovn.org/egressip-mark\\\":{}}},\\\"f:status\\\":{\\\".\\\":{},\\\"f:items\\\":{}}},\\\"manager\\\":\\\"ovn-control-plane\\\",\\\"operation\\\":\\\"Update\\\",\\\"time\\\":\\\"2025-10-30T08:56:56Z\\\"}],\\\"name\\\":\\\"egressip-2\\\",\\\"resourceVersion\\\":\\\"41123\\\",\\\"uid\\\":\\\"3888ee49-72a0-481c-863d-ff715c690f50\\\"},\\\"spec\\\":{\\\"egressIPs\\\":[\\\"172.18.0.101\\\"],\\\"namespaceSelector\\\":{\\\"matchLabels\\\":{\\\"name\\\":\\\"test\\\"}}},\\\"status\\\":{\\\"items\\\":[{\\\"egressIP\\\":\\\"172.18.0.101\\\",\\\"node\\\":\\\"ovn-worker2\\\"}]}}\"}]}"
    }
  ],
  "structuredContent": {
    "resources": [
      {
        "data": "{\"apiVersion\":\"k8s.ovn.org/v1\",\"kind\":\"EgressIP\",\"metadata\":{\"annotations\":{\"k8s.ovn.org/egressip-mark\":\"50000\"},\"creationTimestamp\":\"2025-10-30T08:56:56Z\",\"generation\":2,\"managedFields\":[{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:spec\":{\".\":{},\"f:egressIPs\":{},\"f:namespaceSelector\":{}}},\"manager\":\"kubectl-create\",\"operation\":\"Update\",\"time\":\"2025-10-30T08:56:56Z\"},{\"apiVersion\":\"k8s.ovn.org/v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:annotations\":{\".\":{},\"f:k8s.ovn.org/egressip-mark\":{}}},\"f:status\":{\".\":{},\"f:items\":{}}},\"manager\":\"ovn-control-plane\",\"operation\":\"Update\",\"time\":\"2025-10-30T08:56:56Z\"}],\"name\":\"egressip-2\",\"resourceVersion\":\"41123\",\"uid\":\"3888ee49-72a0-481c-863d-ff715c690f50\"},\"spec\":{\"egressIPs\":[\"172.18.0.101\"],\"namespaceSelector\":{\"matchLabels\":{\"name\":\"test\"}}},\"status\":{\"items\":[{\"egressIP\":\"172.18.0.101\",\"node\":\"ovn-worker2\"}]}}"
      }
    ]
  }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huiran0826 I have added another test case which fetches using outputType=json. PTAL. The unmarshalling into the secret resource works fine.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @arkadeepsen!
LGTM

)

// DebugNode debugs a node by name, image and command.
func (s *MCPServer) DebugNode(ctx context.Context, req *mcp.CallToolRequest, in types.DebugNodeParams) (*mcp.CallToolResult, types.DebugNodeResult, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as ExecPod, DebugNode is not exposed as a tool as it needs write permissions. This will be internally used by other layers like kernel layer, for invoking different commands.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that we have code that is not used anywhere but if you say this will be used very soon I am ok with that.

Copy link

@kyrtapz kyrtapz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Thanks @arkadeepsen!

Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
Signed-off-by: arkadeepsen <arsen@redhat.com>
@arkadeepsen
Copy link
Author

Last push was a minor update to a log message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Kubernetes layer of OVNK MCP server

8 participants