Skip to content

feat: added tool annotations #89

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

Merged
merged 1 commit into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/mcp/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ func (s *Server) initConfiguration() []server.ServerTool {
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. "+
"(Optional, default true)")),
// Tool annotations
mcp.WithTitleAnnotation("Configuration: View"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), s.configurationView},
}
return tools
Expand Down
4 changes: 4 additions & 0 deletions pkg/mcp/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ func (s *Server) initEvents() []server.ServerTool {
mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"),
mcp.WithString("namespace",
mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")),
// Tool annotations
mcp.WithTitleAnnotation("Events: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), s.eventsList},
}
}
Expand Down
16 changes: 16 additions & 0 deletions pkg/mcp/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,32 @@ func (s *Server) initHelm() []server.ServerTool {
mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")),
mcp.WithString("name", mcp.Description("Name of the Helm release (Optional, random name if not provided)")),
mcp.WithString("namespace", mcp.Description("Namespace to install the Helm chart in (Optional, current namespace if not provided)")),
// Tool annotations
mcp.WithTitleAnnotation("Helm: Install"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(false),
mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
mcp.WithOpenWorldHintAnnotation(true),
), s.helmInstall},
{mcp.NewTool("helm_list",
mcp.WithDescription("List all the Helm releases in the current or provided namespace (or in all namespaces if specified)"),
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")),
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")),
// Tool annotations
mcp.WithTitleAnnotation("Helm: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), s.helmList},
{mcp.NewTool("helm_uninstall",
mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"),
mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()),
mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")),
// Tool annotations
mcp.WithTitleAnnotation("Helm: Uninstall"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(true),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), s.helmUninstall},
}
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/mcp/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ func (s *Server) initNamespaces() []server.ServerTool {
ret = append(ret, server.ServerTool{
Tool: mcp.NewTool("namespaces_list",
mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"),
// Tool annotations
mcp.WithTitleAnnotation("Namespaces: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.namespacesList,
})
if s.k.IsOpenShift(context.Background()) {
ret = append(ret, server.ServerTool{
Tool: mcp.NewTool("projects_list",
mcp.WithDescription("List all the OpenShift projects in the current cluster"),
// Tool annotations
mcp.WithTitleAnnotation("Projects: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.projectsList,
})
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/mcp/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,39 @@ func (s *Server) initPods() []server.ServerTool {
{Tool: mcp.NewTool("pods_list",
mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"),
mcp.WithString("labelSelector", mcp.Description("Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsListInAllNamespaces},
{Tool: mcp.NewTool("pods_list_in_namespace",
mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"),
mcp.WithString("namespace", mcp.Description("Namespace to list pods from"), mcp.Required()),
mcp.WithString("labelSelector", mcp.Description("Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: List in Namespace"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsListInNamespace},
{Tool: mcp.NewTool("pods_get",
mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod from")),
mcp.WithString("name", mcp.Description("Name of the Pod"), mcp.Required()),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Get"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsGet},
{Tool: mcp.NewTool("pods_delete",
mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to delete the Pod from")),
mcp.WithString("name", mcp.Description("Name of the Pod to delete"), mcp.Required()),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Delete"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(true),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsDelete},
{Tool: mcp.NewTool("pods_exec",
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
Expand All @@ -48,19 +66,35 @@ func (s *Server) initPods() []server.ServerTool {
mcp.Required(),
),
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Exec"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod
mcp.WithIdempotentHintAnnotation(false),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsExec},
{Tool: mcp.NewTool("pods_log",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Log"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsLog},
{Tool: mcp.NewTool("pods_run",
mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"),
mcp.WithString("namespace", mcp.Description("Namespace to run the Pod in")),
mcp.WithString("name", mcp.Description("Name of the Pod (Optional, random name if not provided)")),
mcp.WithString("image", mcp.Description("Container Image to run in the Pod"), mcp.Required()),
mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Run"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(false),
mcp.WithIdempotentHintAnnotation(false),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsRun},
}
}
Expand Down
24 changes: 22 additions & 2 deletions pkg/mcp/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ func (s *Server) initResources() []server.ServerTool {
mcp.WithString("namespace",
mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces")),
mcp.WithString("labelSelector",
mcp.Description("Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"))),
Handler: s.resourcesList},
mcp.Description("Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
// Tool annotations
mcp.WithTitleAnnotation("Resources: List"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.resourcesList},
{Tool: mcp.NewTool("resources_get",
mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
commonApiVersion),
Expand All @@ -48,6 +52,10 @@ func (s *Server) initResources() []server.ServerTool {
mcp.Description("Optional Namespace to retrieve the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will get resource from configured namespace"),
),
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()),
// Tool annotations
mcp.WithTitleAnnotation("Resources: Get"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.resourcesGet},
{Tool: mcp.NewTool("resources_create_or_update",
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+
Expand All @@ -56,6 +64,12 @@ func (s *Server) initResources() []server.ServerTool {
mcp.Description("A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec"),
mcp.Required(),
),
// Tool annotations
mcp.WithTitleAnnotation("Resources: Create or Update"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(true),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.resourcesCreateOrUpdate},
{Tool: mcp.NewTool("resources_delete",
mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
Expand All @@ -72,6 +86,12 @@ func (s *Server) initResources() []server.ServerTool {
mcp.Description("Optional Namespace to delete the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will delete resource from configured namespace"),
),
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()),
// Tool annotations
mcp.WithTitleAnnotation("Resources: Delete"),
mcp.WithReadOnlyHintAnnotation(false),
mcp.WithDestructiveHintAnnotation(true),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.resourcesDelete},
}
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/mcp/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,17 @@ func TestResourcesDelete(t *testing.T) {
return
}
})
t.Run("resources_delete with nonexistent resource returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "nonexistent-configmap"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(mcp.TextContent).Text != `failed to delete resource: configmaps "nonexistent-configmap" not found` {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(mcp.TextContent).Text)
return
}
})
resourcesDeleteCm, err := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "a-configmap-to-delete"})
t.Run("resources_delete with valid namespaced resource returns success", func(t *testing.T) {
if err != nil {
Expand Down
Loading