Skip to content

Commit 3753f98

Browse files
authored
feat: added tool annotations
1 parent 2994699 commit 3753f98

File tree

7 files changed

+99
-2
lines changed

7 files changed

+99
-2
lines changed

pkg/mcp/configuration.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ func (s *Server) initConfiguration() []server.ServerTool {
1515
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+
1616
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. "+
1717
"(Optional, default true)")),
18+
// Tool annotations
19+
mcp.WithTitleAnnotation("Configuration: View"),
20+
mcp.WithReadOnlyHintAnnotation(true),
21+
mcp.WithOpenWorldHintAnnotation(true),
1822
), s.configurationView},
1923
}
2024
return tools

pkg/mcp/events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ func (s *Server) initEvents() []server.ServerTool {
1313
mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"),
1414
mcp.WithString("namespace",
1515
mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")),
16+
// Tool annotations
17+
mcp.WithTitleAnnotation("Events: List"),
18+
mcp.WithReadOnlyHintAnnotation(true),
19+
mcp.WithOpenWorldHintAnnotation(true),
1620
), s.eventsList},
1721
}
1822
}

pkg/mcp/helm.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,32 @@ func (s *Server) initHelm() []server.ServerTool {
1515
mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")),
1616
mcp.WithString("name", mcp.Description("Name of the Helm release (Optional, random name if not provided)")),
1717
mcp.WithString("namespace", mcp.Description("Namespace to install the Helm chart in (Optional, current namespace if not provided)")),
18+
// Tool annotations
19+
mcp.WithTitleAnnotation("Helm: Install"),
20+
mcp.WithReadOnlyHintAnnotation(false),
21+
mcp.WithDestructiveHintAnnotation(false),
22+
mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
23+
mcp.WithOpenWorldHintAnnotation(true),
1824
), s.helmInstall},
1925
{mcp.NewTool("helm_list",
2026
mcp.WithDescription("List all the Helm releases in the current or provided namespace (or in all namespaces if specified)"),
2127
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")),
2228
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")),
29+
// Tool annotations
30+
mcp.WithTitleAnnotation("Helm: List"),
31+
mcp.WithReadOnlyHintAnnotation(true),
32+
mcp.WithOpenWorldHintAnnotation(true),
2333
), s.helmList},
2434
{mcp.NewTool("helm_uninstall",
2535
mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"),
2636
mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()),
2737
mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")),
38+
// Tool annotations
39+
mcp.WithTitleAnnotation("Helm: Uninstall"),
40+
mcp.WithReadOnlyHintAnnotation(false),
41+
mcp.WithDestructiveHintAnnotation(true),
42+
mcp.WithIdempotentHintAnnotation(true),
43+
mcp.WithOpenWorldHintAnnotation(true),
2844
), s.helmUninstall},
2945
}
3046
}

pkg/mcp/namespaces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ func (s *Server) initNamespaces() []server.ServerTool {
1212
ret = append(ret, server.ServerTool{
1313
Tool: mcp.NewTool("namespaces_list",
1414
mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"),
15+
// Tool annotations
16+
mcp.WithTitleAnnotation("Namespaces: List"),
17+
mcp.WithReadOnlyHintAnnotation(true),
18+
mcp.WithOpenWorldHintAnnotation(true),
1519
), Handler: s.namespacesList,
1620
})
1721
if s.k.IsOpenShift(context.Background()) {
1822
ret = append(ret, server.ServerTool{
1923
Tool: mcp.NewTool("projects_list",
2024
mcp.WithDescription("List all the OpenShift projects in the current cluster"),
25+
// Tool annotations
26+
mcp.WithTitleAnnotation("Projects: List"),
27+
mcp.WithReadOnlyHintAnnotation(true),
28+
mcp.WithOpenWorldHintAnnotation(true),
2129
), Handler: s.projectsList,
2230
})
2331
}

pkg/mcp/pods.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,39 @@ func (s *Server) initPods() []server.ServerTool {
1414
{Tool: mcp.NewTool("pods_list",
1515
mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"),
1616
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]")),
17+
// Tool annotations
18+
mcp.WithTitleAnnotation("Pods: List"),
19+
mcp.WithReadOnlyHintAnnotation(true),
20+
mcp.WithOpenWorldHintAnnotation(true),
1721
), Handler: s.podsListInAllNamespaces},
1822
{Tool: mcp.NewTool("pods_list_in_namespace",
1923
mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"),
2024
mcp.WithString("namespace", mcp.Description("Namespace to list pods from"), mcp.Required()),
2125
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]")),
26+
// Tool annotations
27+
mcp.WithTitleAnnotation("Pods: List in Namespace"),
28+
mcp.WithReadOnlyHintAnnotation(true),
29+
mcp.WithOpenWorldHintAnnotation(true),
2230
), Handler: s.podsListInNamespace},
2331
{Tool: mcp.NewTool("pods_get",
2432
mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"),
2533
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod from")),
2634
mcp.WithString("name", mcp.Description("Name of the Pod"), mcp.Required()),
35+
// Tool annotations
36+
mcp.WithTitleAnnotation("Pods: Get"),
37+
mcp.WithReadOnlyHintAnnotation(true),
38+
mcp.WithOpenWorldHintAnnotation(true),
2739
), Handler: s.podsGet},
2840
{Tool: mcp.NewTool("pods_delete",
2941
mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"),
3042
mcp.WithString("namespace", mcp.Description("Namespace to delete the Pod from")),
3143
mcp.WithString("name", mcp.Description("Name of the Pod to delete"), mcp.Required()),
44+
// Tool annotations
45+
mcp.WithTitleAnnotation("Pods: Delete"),
46+
mcp.WithReadOnlyHintAnnotation(false),
47+
mcp.WithDestructiveHintAnnotation(true),
48+
mcp.WithIdempotentHintAnnotation(true),
49+
mcp.WithOpenWorldHintAnnotation(true),
3250
), Handler: s.podsDelete},
3351
{Tool: mcp.NewTool("pods_exec",
3452
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
@@ -48,19 +66,35 @@ func (s *Server) initPods() []server.ServerTool {
4866
mcp.Required(),
4967
),
5068
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
69+
// Tool annotations
70+
mcp.WithTitleAnnotation("Pods: Exec"),
71+
mcp.WithReadOnlyHintAnnotation(false),
72+
mcp.WithDestructiveHintAnnotation(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod
73+
mcp.WithIdempotentHintAnnotation(false),
74+
mcp.WithOpenWorldHintAnnotation(true),
5175
), Handler: s.podsExec},
5276
{Tool: mcp.NewTool("pods_log",
5377
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
5478
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
5579
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
5680
mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")),
81+
// Tool annotations
82+
mcp.WithTitleAnnotation("Pods: Log"),
83+
mcp.WithReadOnlyHintAnnotation(true),
84+
mcp.WithOpenWorldHintAnnotation(true),
5785
), Handler: s.podsLog},
5886
{Tool: mcp.NewTool("pods_run",
5987
mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"),
6088
mcp.WithString("namespace", mcp.Description("Namespace to run the Pod in")),
6189
mcp.WithString("name", mcp.Description("Name of the Pod (Optional, random name if not provided)")),
6290
mcp.WithString("image", mcp.Description("Container Image to run in the Pod"), mcp.Required()),
6391
mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)")),
92+
// Tool annotations
93+
mcp.WithTitleAnnotation("Pods: Run"),
94+
mcp.WithReadOnlyHintAnnotation(false),
95+
mcp.WithDestructiveHintAnnotation(false),
96+
mcp.WithIdempotentHintAnnotation(false),
97+
mcp.WithOpenWorldHintAnnotation(true),
6498
), Handler: s.podsRun},
6599
}
66100
}

pkg/mcp/resources.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ func (s *Server) initResources() []server.ServerTool {
3131
mcp.WithString("namespace",
3232
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")),
3333
mcp.WithString("labelSelector",
34-
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]"))),
35-
Handler: s.resourcesList},
34+
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]")),
35+
// Tool annotations
36+
mcp.WithTitleAnnotation("Resources: List"),
37+
mcp.WithReadOnlyHintAnnotation(true),
38+
mcp.WithOpenWorldHintAnnotation(true),
39+
), Handler: s.resourcesList},
3640
{Tool: mcp.NewTool("resources_get",
3741
mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
3842
commonApiVersion),
@@ -48,6 +52,10 @@ func (s *Server) initResources() []server.ServerTool {
4852
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"),
4953
),
5054
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()),
55+
// Tool annotations
56+
mcp.WithTitleAnnotation("Resources: Get"),
57+
mcp.WithReadOnlyHintAnnotation(true),
58+
mcp.WithOpenWorldHintAnnotation(true),
5159
), Handler: s.resourcesGet},
5260
{Tool: mcp.NewTool("resources_create_or_update",
5361
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+
@@ -56,6 +64,12 @@ func (s *Server) initResources() []server.ServerTool {
5664
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"),
5765
mcp.Required(),
5866
),
67+
// Tool annotations
68+
mcp.WithTitleAnnotation("Resources: Create or Update"),
69+
mcp.WithReadOnlyHintAnnotation(false),
70+
mcp.WithDestructiveHintAnnotation(true),
71+
mcp.WithIdempotentHintAnnotation(true),
72+
mcp.WithOpenWorldHintAnnotation(true),
5973
), Handler: s.resourcesCreateOrUpdate},
6074
{Tool: mcp.NewTool("resources_delete",
6175
mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
@@ -72,6 +86,12 @@ func (s *Server) initResources() []server.ServerTool {
7286
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"),
7387
),
7488
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()),
89+
// Tool annotations
90+
mcp.WithTitleAnnotation("Resources: Delete"),
91+
mcp.WithReadOnlyHintAnnotation(false),
92+
mcp.WithDestructiveHintAnnotation(true),
93+
mcp.WithIdempotentHintAnnotation(true),
94+
mcp.WithOpenWorldHintAnnotation(true),
7595
), Handler: s.resourcesDelete},
7696
}
7797
}

pkg/mcp/resources_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,17 @@ func TestResourcesDelete(t *testing.T) {
476476
return
477477
}
478478
})
479+
t.Run("resources_delete with nonexistent resource returns error", func(t *testing.T) {
480+
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "nonexistent-configmap"})
481+
if !toolResult.IsError {
482+
t.Fatalf("call tool should fail")
483+
return
484+
}
485+
if toolResult.Content[0].(mcp.TextContent).Text != `failed to delete resource: configmaps "nonexistent-configmap" not found` {
486+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(mcp.TextContent).Text)
487+
return
488+
}
489+
})
479490
resourcesDeleteCm, err := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "a-configmap-to-delete"})
480491
t.Run("resources_delete with valid namespaced resource returns success", func(t *testing.T) {
481492
if err != nil {

0 commit comments

Comments
 (0)