-
Notifications
You must be signed in to change notification settings - Fork 7
Add kubernetes layer for the ovnk mcp server #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
8f4c440 to
4f1e222
Compare
| { | ||
| "mcpServers": { | ||
| "ovn-kubernetes": { | ||
| "command": "/PATH-TO-THE-LOCAL-GIT-REPO/_output/ovnk-mcp-server", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
]
}
}
}| gvk := schema.GroupVersionKind{ | ||
| Group: group, | ||
| Version: version, | ||
| Kind: kind, | ||
| } |
There was a problem hiding this comment.
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"]
There was a problem hiding this comment.
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 hintsMCP 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 hintsMCP 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.
pkg/kubernetes/client/resources.go
Outdated
| ) | ||
|
|
||
| // GetResourceGVK gets a resource by group, version, kind, name and namespace. | ||
| func (client *OVNKMCPServerClientSet) GetResourceGVK(group, version, kind, resourceName, namespace string) (*unstructured.Unstructured, error) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
94d363a to
c9a2408
Compare
|
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. |
f71ac59 to
e4499af
Compare
| @@ -0,0 +1,56 @@ | |||
| package client | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/kubernetes/client/nodes.go
Outdated
| "k8s.io/pod-security-admission/api" | ||
| ) | ||
|
|
||
| func (client *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string, isOpenshift bool) (string, string, error) { |
There was a problem hiding this comment.
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
| 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) { |
pkg/kubernetes/client/nodes.go
Outdated
|
|
||
| cleanupNamespace := func() { | ||
| // Delete the namespace. | ||
| client.clientSet.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{}) |
There was a problem hiding this comment.
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.
pkg/kubernetes/client/nodes.go
Outdated
| } | ||
| ns, err := client.clientSet.CoreV1().Namespaces().Create(ctx, tmpNS, metav1.CreateOptions{}) | ||
| if err != nil { | ||
| return "", nil, err |
There was a problem hiding this comment.
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.
| return "", nil, err | |
| return "", nil, errors.Wrap(err, "failed to create temporary namespace") |
pkg/kubernetes/client/nodes.go
Outdated
| Privileged: &isTrue, | ||
| RunAsUser: &zero, |
There was a problem hiding this comment.
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"
| Privileged: &isTrue, | |
| RunAsUser: &zero, | |
| Privileged: ptr.To(true), | |
| RunAsUser: ptr.To(int64(0)), |
| "k8s.io/client-go/tools/remotecommand" | ||
| ) | ||
|
|
||
| func TestGetPodLogs(t *testing.T) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/kubernetes/types/common.go
Outdated
| } | ||
|
|
||
| // GetFormattedJSONData gets the JSON data from a resource. | ||
| func (j *FormattedOutput) GetFormattedJSONData(data any) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func (j *FormattedOutput) GetFormattedJSONData(data any) error { | |
| func (j *FormattedOutput) ToJSON(data any) error { |
pkg/kubernetes/types/common.go
Outdated
| } | ||
|
|
||
| // GetFormattedYAMLData gets the YAML data from a resource. | ||
| func (j *FormattedOutput) GetFormattedYAMLData(data any) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func (j *FormattedOutput) GetFormattedYAMLData(data any) error { | |
| func (j *FormattedOutput) ToYAML(data any) error { |
pkg/kubernetes/resources.go
Outdated
| err = errors.New("version is required") | ||
| } | ||
| if in.Kind == "" { | ||
| errors.Join(err, errors.New("kind is required")) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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()?
There was a problem hiding this comment.
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.
pkg/kubernetes/client/openshift.go
Outdated
| import "strings" | ||
|
|
||
| // checkOpenshift checks if the cluster is an Openshift cluster. | ||
| func (s *OVNKMCPServerClientSet) CheckOpenshift() bool { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
4e4999c to
7a9a82f
Compare
cmd/ovnk-mcp-server/main.go
Outdated
| Mode string | ||
| Transport string | ||
| Port string | ||
| Kubernetes kubernetes.Config |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cmd/ovnk-mcp-server/main.go
Outdated
| switch serverCfg.Transport { | ||
| case "stdio": | ||
| t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr} | ||
| if err := ovnkMcpServer.Run(context.Background(), t); err != nil { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
pkg/kubernetes/client/pods.go
Outdated
|
|
||
| // 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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
pkg/kubernetes/client/resources.go
Outdated
| } | ||
|
|
||
| // GetResourceGVK gets a resource by group, version, kind, name and namespace. | ||
| func (c *OVNKMCPServerClientSet) GetResourceGVK(group, version, kind, resourceName, namespace string) (*unstructured.Unstructured, error) { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gotcha
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
cmd/ovnk-mcp-server/main.go
Outdated
| Mode string | ||
| Transport string | ||
| Port string | ||
| Kubernetes kubernetes.Config |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
+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. |
7a9a82f to
3f924a2
Compare
kyrtapz
left a comment
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
pkg/kubernetes/client/nodes.go
Outdated
| // Create a host networked privileged debug pod. | ||
| debugPod := &corev1.Pod{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "debug-node-" + node + "-" + utilrand.String(5), |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
pkg/kubernetes/client/pods.go
Outdated
| } | ||
|
|
||
| // Cannot exec into a container in a completed pod. | ||
| if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Link to kubectl code: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/exec/exec.go#L350-L352
|
|
||
| // If no container is specified, use the first container. | ||
| if container == "" { | ||
| container = pod.Spec.Containers[0].Name |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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\"}]}}"
}
]
}
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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\"}]}}"
}
]
}
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used anywhere?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
3f924a2 to
e63bf6c
Compare
e63bf6c to
317461c
Compare
kyrtapz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Thanks @arkadeepsen!
317461c to
688d5fc
Compare
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>
688d5fc to
c4e2e7f
Compare
|
Last push was a minor update to a log message. |
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