From 392d29d0ee905bea3f715b86af88947fd9504505 Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Wed, 4 Feb 2026 10:22:08 +0000 Subject: [PATCH] feat: add --read-only flag to disable write operations Add a new `--read-only` CLI flag that disables tools which perform write operations (delete, patch, scale, create, apply, etc.). This enables deploying the MCP server in read-only mode for: - Observability-only use cases (monitoring, troubleshooting) - Environments with read-only service accounts - Compliance requirements separating read/write capabilities Tools are categorized as read-only or write operations: - K8s: 8 read-only, 14 write tools - Helm: 3 read-only, 3 write tools - Istio: 9 read-only, 3 write tools - Cilium: ~25 read-only, ~15 write tools - Argo: 4 read-only, 4 write tools - Prometheus/Kubescape/Utils: all read-only (unchanged) Co-Authored-By: Claude Opus 4.5 Signed-off-by: Matteo Mori --- cmd/main.go | 20 ++- pkg/argo/argo.go | 60 ++++---- pkg/cilium/cilium.go | 305 +++++++++++++++++++++----------------- pkg/cilium/cilium_test.go | 2 +- pkg/helm/helm.go | 61 ++++---- pkg/helm/helm_test.go | 2 +- pkg/istio/istio.go | 42 +++--- pkg/istio/istio_test.go | 2 +- pkg/k8s/k8s.go | 240 +++++++++++++++--------------- 9 files changed, 392 insertions(+), 342 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index c1189e1..3d5bbf2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -39,6 +39,7 @@ var ( tools []string kubeconfig *string showVersion bool + readOnly bool // These variables should be set during build time using -ldflags Name = "kagent-tools-server" @@ -58,6 +59,7 @@ func init() { rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP") rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.") rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit") + rootCmd.Flags().BoolVar(&readOnly, "read-only", false, "Run in read-only mode (disable tools that perform write operations)") kubeconfig = rootCmd.Flags().String("kubeconfig", "", "kubeconfig file path (optional, defaults to in-cluster config)") // if found .env file, load it @@ -119,9 +121,13 @@ func run(cmd *cobra.Command, args []string) { attribute.Bool("server.stdio_mode", stdio), attribute.Int("server.port", port), attribute.StringSlice("server.tools", tools), + attribute.Bool("server.read_only", readOnly), ) logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate) + if readOnly { + logger.Get().Info("Running in read-only mode - write operations are disabled") + } mcp := server.NewMCPServer( Name, @@ -129,7 +135,7 @@ func run(cmd *cobra.Command, args []string) { ) // Register tools - registerMCP(mcp, tools, *kubeconfig) + registerMCP(mcp, tools, *kubeconfig, readOnly) // Create wait group for server goroutines var wg sync.WaitGroup @@ -285,14 +291,14 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) { } } -func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) { +func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) { // A map to hold tool providers and their registration functions toolProviderMap := map[string]func(*server.MCPServer){ - "argo": argo.RegisterTools, - "cilium": cilium.RegisterTools, - "helm": helm.RegisterTools, - "istio": istio.RegisterTools, - "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) }, + "argo": func(s *server.MCPServer) { argo.RegisterTools(s, readOnly) }, + "cilium": func(s *server.MCPServer) { cilium.RegisterTools(s, readOnly) }, + "helm": func(s *server.MCPServer) { helm.RegisterTools(s, readOnly) }, + "istio": func(s *server.MCPServer) { istio.RegisterTools(s, readOnly) }, + "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig, readOnly) }, "kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig) }, "prometheus": prometheus.RegisterTools, "utils": utils.RegisterTools, diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 7edea84..758a4fb 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -375,7 +375,8 @@ func handleListRollouts(ctx context.Context, request mcp.CallToolRequest) (*mcp. return mcp.NewToolResultText(output), nil } -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install", mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"), mcp.WithString("namespace", mcp.Description("The namespace where Argo Rollouts is installed")), @@ -392,36 +393,39 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("type", mcp.Description("What to list: rollouts or experiments"), mcp.DefaultString("rollouts")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_rollouts_list", handleListRollouts))) - s.AddTool(mcp.NewTool("argo_promote_rollout", - mcp.WithDescription("Promote a paused rollout to the next step"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - mcp.WithString("full", mcp.Description("Promote the rollout to the final step")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout))) - - s.AddTool(mcp.NewTool("argo_pause_rollout", - mcp.WithDescription("Pause a rollout"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout))) - - s.AddTool(mcp.NewTool("argo_set_rollout_image", - mcp.WithDescription("Set the image of a rollout"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()), - mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage))) - - s.AddTool(mcp.NewTool("argo_verify_gateway_plugin", - mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"), - mcp.WithString("version", mcp.Description("The version of the plugin to check")), - mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")), - mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin))) - s.AddTool(mcp.NewTool("argo_check_plugin_logs", mcp.WithDescription("Check the logs of the Argo Rollouts Gateway API plugin"), mcp.WithString("namespace", mcp.Description("The namespace of the plugin resources")), mcp.WithString("timeout", mcp.Description("Timeout for log collection in seconds")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_check_plugin_logs", handleCheckPluginLogs))) + + // Write tools - only registered when not in read-only mode + if !readOnly { + s.AddTool(mcp.NewTool("argo_promote_rollout", + mcp.WithDescription("Promote a paused rollout to the next step"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + mcp.WithString("full", mcp.Description("Promote the rollout to the final step")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout))) + + s.AddTool(mcp.NewTool("argo_pause_rollout", + mcp.WithDescription("Pause a rollout"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout))) + + s.AddTool(mcp.NewTool("argo_set_rollout_image", + mcp.WithDescription("Set the image of a rollout"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()), + mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage))) + + s.AddTool(mcp.NewTool("argo_verify_gateway_plugin", + mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"), + mcp.WithString("version", mcp.Description("The version of the plugin to check")), + mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")), + mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin))) + } } diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index 6ad576c..9479378 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -200,41 +200,12 @@ func handleToggleClusterMesh(ctx context.Context, request mcp.CallToolRequest) ( return mcp.NewToolResultText(output), nil } -func RegisterTools(s *server.MCPServer) { - - // Register all Cilium tools (main and debug) +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("cilium_status_and_version", mcp.WithDescription("Get the status and version of Cilium installation"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_status_and_version", handleCiliumStatusAndVersion))) - s.AddTool(mcp.NewTool("cilium_upgrade_cilium", - mcp.WithDescription("Upgrade Cilium on the cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the cluster to upgrade Cilium on")), - mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_upgrade_cilium", handleUpgradeCilium))) - - s.AddTool(mcp.NewTool("cilium_install_cilium", - mcp.WithDescription("Install Cilium on the cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the cluster to install Cilium on")), - mcp.WithString("cluster_id", mcp.Description("The ID of the cluster to install Cilium on")), - mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_install_cilium", handleInstallCilium))) - - s.AddTool(mcp.NewTool("cilium_uninstall_cilium", - mcp.WithDescription("Uninstall Cilium from the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_uninstall_cilium", handleUninstallCilium))) - - s.AddTool(mcp.NewTool("cilium_connect_to_remote_cluster", - mcp.WithDescription("Connect to a remote cluster for cluster mesh"), - mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), - mcp.WithString("context", mcp.Description("The kubectl context for the destination cluster")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_connect_to_remote_cluster", handleConnectToRemoteCluster))) - - s.AddTool(mcp.NewTool("cilium_disconnect_remote_cluster", - mcp.WithDescription("Disconnect from a remote cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_remote_cluster", handleDisconnectRemoteCluster))) - s.AddTool(mcp.NewTool("cilium_list_bgp_peers", mcp.WithDescription("List BGP peers"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bgp_peers", handleListBGPPeers))) @@ -251,15 +222,46 @@ func RegisterTools(s *server.MCPServer) { mcp.WithDescription("Show Cilium features status"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_features_status", handleShowFeaturesStatus))) - s.AddTool(mcp.NewTool("cilium_toggle_hubble", - mcp.WithDescription("Enable or disable Hubble"), - mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_hubble", handleToggleHubble))) - - s.AddTool(mcp.NewTool("cilium_toggle_cluster_mesh", - mcp.WithDescription("Enable or disable cluster mesh"), - mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_cluster_mesh", handleToggleClusterMesh))) + // Write tools - only registered when write operations are enabled + if !readOnly { + s.AddTool(mcp.NewTool("cilium_upgrade_cilium", + mcp.WithDescription("Upgrade Cilium on the cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the cluster to upgrade Cilium on")), + mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_upgrade_cilium", handleUpgradeCilium))) + + s.AddTool(mcp.NewTool("cilium_install_cilium", + mcp.WithDescription("Install Cilium on the cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the cluster to install Cilium on")), + mcp.WithString("cluster_id", mcp.Description("The ID of the cluster to install Cilium on")), + mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_install_cilium", handleInstallCilium))) + + s.AddTool(mcp.NewTool("cilium_uninstall_cilium", + mcp.WithDescription("Uninstall Cilium from the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_uninstall_cilium", handleUninstallCilium))) + + s.AddTool(mcp.NewTool("cilium_connect_to_remote_cluster", + mcp.WithDescription("Connect to a remote cluster for cluster mesh"), + mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), + mcp.WithString("context", mcp.Description("The kubectl context for the destination cluster")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_connect_to_remote_cluster", handleConnectToRemoteCluster))) + + s.AddTool(mcp.NewTool("cilium_disconnect_remote_cluster", + mcp.WithDescription("Disconnect from a remote cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_remote_cluster", handleDisconnectRemoteCluster))) + + s.AddTool(mcp.NewTool("cilium_toggle_hubble", + mcp.WithDescription("Enable or disable Hubble"), + mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_hubble", handleToggleHubble))) + + s.AddTool(mcp.NewTool("cilium_toggle_cluster_mesh", + mcp.WithDescription("Enable or disable cluster mesh"), + mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_cluster_mesh", handleToggleClusterMesh))) + } // Add tools that are also needed by cilium-manager agent s.AddTool(mcp.NewTool("cilium_get_daemon_status", @@ -295,12 +297,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to show the configuration options for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_configuration_options", handleShowConfigurationOptions))) - s.AddTool(mcp.NewTool("cilium_toggle_configuration_option", - mcp.WithDescription("Toggle a Cilium configuration option"), - mcp.WithString("option", mcp.Description("The option to toggle"), mcp.Required()), - mcp.WithString("value", mcp.Description("The value to set the option to (true/false)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to toggle the configuration option for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_configuration_option", handleToggleConfigurationOption))) + // Write tool - toggle_configuration_option + if !readOnly { + s.AddTool(mcp.NewTool("cilium_toggle_configuration_option", + mcp.WithDescription("Toggle a Cilium configuration option"), + mcp.WithString("option", mcp.Description("The option to toggle"), mcp.Required()), + mcp.WithString("value", mcp.Description("The value to set the option to (true/false)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to toggle the configuration option for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_configuration_option", handleToggleConfigurationOption))) + } s.AddTool(mcp.NewTool("cilium_list_services", mcp.WithDescription("List services for the cluster"), @@ -314,31 +319,34 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the service information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_service_information", handleGetServiceInformation))) - s.AddTool(mcp.NewTool("cilium_update_service", - mcp.WithDescription("Update a service in the cluster"), - mcp.WithString("backend_weights", mcp.Description("The backend weights to update the service with")), - mcp.WithString("backends", mcp.Description("The backends to update the service with"), mcp.Required()), - mcp.WithString("frontend", mcp.Description("The frontend to update the service with"), mcp.Required()), - mcp.WithString("id", mcp.Description("The ID of the service to update"), mcp.Required()), - mcp.WithString("k8s_cluster_internal", mcp.Description("Whether to update the k8s cluster internal flag")), - mcp.WithString("k8s_ext_traffic_policy", mcp.Description("The k8s ext traffic policy to update the service with")), - mcp.WithString("k8s_external", mcp.Description("Whether to update the k8s external flag")), - mcp.WithString("k8s_host_port", mcp.Description("Whether to update the k8s host port flag")), - mcp.WithString("k8s_int_traffic_policy", mcp.Description("The k8s int traffic policy to update the service with")), - mcp.WithString("k8s_load_balancer", mcp.Description("Whether to update the k8s load balancer flag")), - mcp.WithString("k8s_node_port", mcp.Description("Whether to update the k8s node port flag")), - mcp.WithString("local_redirect", mcp.Description("Whether to update the local redirect flag")), - mcp.WithString("protocol", mcp.Description("The protocol to update the service with")), - mcp.WithString("states", mcp.Description("The states to update the service with")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_service", handleUpdateService))) - - s.AddTool(mcp.NewTool("cilium_delete_service", - mcp.WithDescription("Delete a service from the cluster"), - mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), - mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_service", handleDeleteService))) + // Write tools - service management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_update_service", + mcp.WithDescription("Update a service in the cluster"), + mcp.WithString("backend_weights", mcp.Description("The backend weights to update the service with")), + mcp.WithString("backends", mcp.Description("The backends to update the service with"), mcp.Required()), + mcp.WithString("frontend", mcp.Description("The frontend to update the service with"), mcp.Required()), + mcp.WithString("id", mcp.Description("The ID of the service to update"), mcp.Required()), + mcp.WithString("k8s_cluster_internal", mcp.Description("Whether to update the k8s cluster internal flag")), + mcp.WithString("k8s_ext_traffic_policy", mcp.Description("The k8s ext traffic policy to update the service with")), + mcp.WithString("k8s_external", mcp.Description("Whether to update the k8s external flag")), + mcp.WithString("k8s_host_port", mcp.Description("Whether to update the k8s host port flag")), + mcp.WithString("k8s_int_traffic_policy", mcp.Description("The k8s int traffic policy to update the service with")), + mcp.WithString("k8s_load_balancer", mcp.Description("Whether to update the k8s load balancer flag")), + mcp.WithString("k8s_node_port", mcp.Description("Whether to update the k8s node port flag")), + mcp.WithString("local_redirect", mcp.Description("Whether to update the local redirect flag")), + mcp.WithString("protocol", mcp.Description("The protocol to update the service with")), + mcp.WithString("states", mcp.Description("The states to update the service with")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_service", handleUpdateService))) + + s.AddTool(mcp.NewTool("cilium_delete_service", + mcp.WithDescription("Delete a service from the cluster"), + mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), + mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_service", handleDeleteService))) + } // Debug tools (previously in RegisterCiliumDbgTools) s.AddTool(mcp.NewTool("cilium_get_endpoint_details", @@ -361,26 +369,29 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint health for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_health", handleGetEndpointHealth))) - s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", - mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), - mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), - mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_labels", handleManageEndpointLabels))) - - s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", - mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), - mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_config", handleManageEndpointConfiguration))) - - s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", - mcp.WithDescription("Disconnect an endpoint from the network"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_endpoint", handleDisconnectEndpoint))) + // Write tools - endpoint management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", + mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), + mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), + mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_labels", handleManageEndpointLabels))) + + s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", + mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), + mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_config", handleManageEndpointConfiguration))) + + s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", + mcp.WithDescription("Disconnect an endpoint from the network"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_endpoint", handleDisconnectEndpoint))) + } s.AddTool(mcp.NewTool("cilium_list_identities", mcp.WithDescription("List all identities in the cluster"), @@ -403,10 +414,13 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the encryption state for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_encryption_state", handleDisplayEncryptionState))) - s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", - mcp.WithDescription("Flush the IPsec state for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_flush_ipsec_state", handleFlushIPsecState))) + // Write tool - flush_ipsec_state + if !readOnly { + s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", + mcp.WithDescription("Flush the IPsec state for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_flush_ipsec_state", handleFlushIPsecState))) + } s.AddTool(mcp.NewTool("cilium_list_envoy_config", mcp.WithDescription("List the Envoy configuration for a resource in the cluster"), @@ -437,11 +451,14 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the IP cache information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_ip_cache_information", handleShowIPCacheInformation))) - s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", - mcp.WithDescription("Delete a key from the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_key_from_kv_store", handleDeleteKeyFromKVStore))) + // Write tool - delete_key_from_kv_store + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", + mcp.WithDescription("Delete a key from the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_key_from_kv_store", handleDeleteKeyFromKVStore))) + } s.AddTool(mcp.NewTool("cilium_get_kv_store_key", mcp.WithDescription("Get a key from the kvstore for the cluster"), @@ -449,12 +466,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the key from")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_kv_store_key", handleGetKVStoreKey))) - s.AddTool(mcp.NewTool("cilium_set_kv_store_key", - mcp.WithDescription("Set a key in the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), - mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_set_kv_store_key", handleSetKVStoreKey))) + // Write tool - set_kv_store_key + if !readOnly { + s.AddTool(mcp.NewTool("cilium_set_kv_store_key", + mcp.WithDescription("Set a key in the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), + mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_set_kv_store_key", handleSetKVStoreKey))) + } s.AddTool(mcp.NewTool("cilium_show_load_information", mcp.WithDescription("Show load information for the cluster"), @@ -505,12 +525,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get policy node information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_policy_node_information", handleDisplayPolicyNodeInformation))) - s.AddTool(mcp.NewTool("cilium_delete_policy_rules", - mcp.WithDescription("Delete policy rules for the cluster"), - mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), - mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_policy_rules", handleDeletePolicyRules))) + // Write tool - delete_policy_rules + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_policy_rules", + mcp.WithDescription("Delete policy rules for the cluster"), + mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), + mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_policy_rules", handleDeletePolicyRules))) + } s.AddTool(mcp.NewTool("cilium_display_selectors", mcp.WithDescription("Display selectors for the cluster"), @@ -522,19 +545,22 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the XDP CIDR filters for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_xdp_cidr_filters", handleListXDPCIDRFilters))) - s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", - mcp.WithDescription("Update XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_xdp_cidr_filters", handleUpdateXDPCIDRFilters))) + // Write tools - XDP CIDR filters + if !readOnly { + s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", + mcp.WithDescription("Update XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_xdp_cidr_filters", handleUpdateXDPCIDRFilters))) - s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", - mcp.WithDescription("Delete XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_xdp_cidr_filters", handleDeleteXDPCIDRFilters))) + s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", + mcp.WithDescription("Delete XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_xdp_cidr_filters", handleDeleteXDPCIDRFilters))) + } s.AddTool(mcp.NewTool("cilium_validate_cilium_network_policies", mcp.WithDescription("Validate Cilium network policies for the cluster"), @@ -554,20 +580,23 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorder for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_pcap_recorder", handleGetPCAPRecorder))) - s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", - mcp.WithDescription("Delete a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_pcap_recorder", handleDeletePCAPRecorder))) - - s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", - mcp.WithDescription("Update a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), - mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), - mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), - mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_pcap_recorder", handleUpdatePCAPRecorder))) + // Write tools - PCAP recorder management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", + mcp.WithDescription("Delete a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_pcap_recorder", handleDeletePCAPRecorder))) + + s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", + mcp.WithDescription("Update a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), + mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), + mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), + mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_pcap_recorder", handleUpdatePCAPRecorder))) + } } // -- Debug Tools -- diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index b7827de..5e4ec24 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -16,7 +16,7 @@ import ( func TestRegisterCiliumTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations // We can't directly check the tools, but we can ensure the call doesn't panic } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 0bf1a4c..c8a6b91 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -288,8 +288,8 @@ func handleHelmRepoUpdate(ctx context.Context, request mcp.CallToolRequest) (*mc } // Register Helm tools -func RegisterTools(s *server.MCPServer) { - +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("helm_list_releases", mcp.WithDescription("List Helm releases in a namespace"), mcp.WithString("namespace", mcp.Description("The namespace to list releases from")), @@ -311,34 +311,37 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("resource", mcp.Description("The resource to get (all, hooks, manifest, notes, values)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_get_release", handleHelmGetRelease))) - s.AddTool(mcp.NewTool("helm_upgrade", - mcp.WithDescription("Upgrade or install a Helm release"), - mcp.WithString("name", mcp.Description("The name of the release"), mcp.Required()), - mcp.WithString("chart", mcp.Description("The chart to install or upgrade to"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the release")), - mcp.WithString("version", mcp.Description("The version of the chart to upgrade to")), - mcp.WithString("values", mcp.Description("Path to a values file")), - mcp.WithString("set", mcp.Description("Set values on the command line (e.g., 'key1=val1,key2=val2')")), - mcp.WithString("install", mcp.Description("Run an install if the release is not present")), - mcp.WithString("dry_run", mcp.Description("Simulate an upgrade")), - mcp.WithString("wait", mcp.Description("Wait for the upgrade to complete")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_upgrade", handleHelmUpgradeRelease))) - - s.AddTool(mcp.NewTool("helm_uninstall", - mcp.WithDescription("Uninstall a Helm release"), - mcp.WithString("name", mcp.Description("The name of the release to uninstall"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), - mcp.WithString("dry_run", mcp.Description("Simulate an uninstall")), - mcp.WithString("wait", mcp.Description("Wait for the uninstall to complete")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_uninstall", handleHelmUninstall))) - - s.AddTool(mcp.NewTool("helm_repo_add", - mcp.WithDescription("Add a Helm repository"), - mcp.WithString("name", mcp.Description("The name of the repository"), mcp.Required()), - mcp.WithString("url", mcp.Description("The URL of the repository"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_add", handleHelmRepoAdd))) - s.AddTool(mcp.NewTool("helm_repo_update", mcp.WithDescription("Update information of available charts locally from chart repositories"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_update", handleHelmRepoUpdate))) + + // Write tools - only registered when not in read-only mode + if !readOnly { + s.AddTool(mcp.NewTool("helm_upgrade", + mcp.WithDescription("Upgrade or install a Helm release"), + mcp.WithString("name", mcp.Description("The name of the release"), mcp.Required()), + mcp.WithString("chart", mcp.Description("The chart to install or upgrade to"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the release")), + mcp.WithString("version", mcp.Description("The version of the chart to upgrade to")), + mcp.WithString("values", mcp.Description("Path to a values file")), + mcp.WithString("set", mcp.Description("Set values on the command line (e.g., 'key1=val1,key2=val2')")), + mcp.WithString("install", mcp.Description("Run an install if the release is not present")), + mcp.WithString("dry_run", mcp.Description("Simulate an upgrade")), + mcp.WithString("wait", mcp.Description("Wait for the upgrade to complete")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_upgrade", handleHelmUpgradeRelease))) + + s.AddTool(mcp.NewTool("helm_uninstall", + mcp.WithDescription("Uninstall a Helm release"), + mcp.WithString("name", mcp.Description("The name of the release to uninstall"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), + mcp.WithString("dry_run", mcp.Description("Simulate an uninstall")), + mcp.WithString("wait", mcp.Description("Wait for the uninstall to complete")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_uninstall", handleHelmUninstall))) + + s.AddTool(mcp.NewTool("helm_repo_add", + mcp.WithDescription("Add a Helm repository"), + mcp.WithString("name", mcp.Description("The name of the repository"), mcp.Required()), + mcp.WithString("url", mcp.Description("The URL of the repository"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_add", handleHelmRepoAdd))) + } } diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 28dca31..9e5b26c 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -13,7 +13,7 @@ import ( func TestRegisterTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations } // Test Helm List Releases diff --git a/pkg/istio/istio.go b/pkg/istio/istio.go index 680d83c..dd1958c 100644 --- a/pkg/istio/istio.go +++ b/pkg/istio/istio.go @@ -298,7 +298,8 @@ func handleZtunnelConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp } // Register Istio tools -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered // Istio proxy status s.AddTool(mcp.NewTool("istio_proxy_status", @@ -315,13 +316,7 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("config_type", mcp.Description("Type of configuration (all, bootstrap, cluster, ecds, listener, log, route, secret)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_proxy_config", handleIstioProxyConfig))) - // Istio install - s.AddTool(mcp.NewTool("istio_install_istio", - mcp.WithDescription("Install Istio with a specified configuration profile"), - mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_install_istio", handleIstioInstall))) - - // Istio generate manifest + // Istio generate manifest (read-only - just generates YAML, doesn't apply) s.AddTool(mcp.NewTool("istio_generate_manifest", mcp.WithDescription("Generate Istio manifest for a given profile"), mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), @@ -347,21 +342,11 @@ func RegisterTools(s *server.MCPServer) { mcp.WithDescription("List all waypoints in the mesh"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_list_waypoints", handleWaypointList))) - // Waypoint generate + // Waypoint generate (read-only - just generates YAML, doesn't apply) s.AddTool(mcp.NewTool("istio_generate_waypoint", mcp.WithDescription("Generate a waypoint resource YAML"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_generate_waypoint", handleWaypointGenerate))) - // Waypoint apply - s.AddTool(mcp.NewTool("istio_apply_waypoint", - mcp.WithDescription("Apply a waypoint resource to the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_apply_waypoint", handleWaypointApply))) - - // Waypoint delete - s.AddTool(mcp.NewTool("istio_delete_waypoint", - mcp.WithDescription("Delete a waypoint resource from the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_delete_waypoint", handleWaypointDelete))) - // Waypoint status s.AddTool(mcp.NewTool("istio_waypoint_status", mcp.WithDescription("Get the status of a waypoint resource"), @@ -371,4 +356,23 @@ func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("istio_ztunnel_config", mcp.WithDescription("Get the ztunnel configuration for a namespace"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_ztunnel_config", handleZtunnelConfig))) + + // Write tools - only registered when write operations are enabled + if !readOnly { + // Istio install + s.AddTool(mcp.NewTool("istio_install_istio", + mcp.WithDescription("Install Istio with a specified configuration profile"), + mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_install_istio", handleIstioInstall))) + + // Waypoint apply + s.AddTool(mcp.NewTool("istio_apply_waypoint", + mcp.WithDescription("Apply a waypoint resource to the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_apply_waypoint", handleWaypointApply))) + + // Waypoint delete + s.AddTool(mcp.NewTool("istio_delete_waypoint", + mcp.WithDescription("Delete a waypoint resource from the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_delete_waypoint", handleWaypointDelete))) + } } diff --git a/pkg/istio/istio_test.go b/pkg/istio/istio_test.go index d2503c9..36abeef 100644 --- a/pkg/istio/istio_test.go +++ b/pkg/istio/istio_test.go @@ -13,7 +13,7 @@ import ( func TestRegisterTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations } func TestHandleIstioProxyStatus(t *testing.T) { diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index c8e9085..f9184d1 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -558,9 +558,10 @@ func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, timeout time } // RegisterK8sTools registers all k8s tools with the MCP server -func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { +func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readOnly bool) { k8sTool := NewK8sToolWithConfig(kubeconfig, llm) + // Read-only tools - always registered s.AddTool(mcp.NewTool("k8s_get_resources", mcp.WithDescription("Get Kubernetes resources using kubectl"), mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), @@ -578,52 +579,11 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithNumber("tail_lines", mcp.Description("Number of lines to show from the end (default: 50)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_pod_logs", k8sTool.handleKubectlLogsEnhanced))) - s.AddTool(mcp.NewTool("k8s_scale", - mcp.WithDescription("Scale a Kubernetes deployment"), - mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), - mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_scale", k8sTool.handleScaleDeployment))) - - s.AddTool(mcp.NewTool("k8s_patch_resource", - mcp.WithDescription("Patch a Kubernetes resource using strategic merge patch"), - mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, etc.)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), - mcp.WithString("patch", mcp.Description("JSON patch to apply"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) - - s.AddTool(mcp.NewTool("k8s_apply_manifest", - mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), - mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_apply_manifest", k8sTool.handleApplyManifest))) - - s.AddTool(mcp.NewTool("k8s_delete_resource", - mcp.WithDescription("Delete a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_delete_resource", k8sTool.handleDeleteResource))) - - s.AddTool(mcp.NewTool("k8s_check_service_connectivity", - mcp.WithDescription("Check connectivity to a service using a temporary curl pod"), - mcp.WithString("service_name", mcp.Description("Service name to test (e.g., my-service.my-namespace.svc.cluster.local:80)"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace to run the check from (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_check_service_connectivity", k8sTool.handleCheckServiceConnectivity))) - s.AddTool(mcp.NewTool("k8s_get_events", mcp.WithDescription("Get events from a Kubernetes namespace"), mcp.WithString("namespace", mcp.Description("Namespace to get events from (default: default)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_events", k8sTool.handleGetEvents))) - s.AddTool(mcp.NewTool("k8s_execute_command", - mcp.WithDescription("Execute a command in a Kubernetes pod"), - mcp.WithString("pod_name", mcp.Description("Name of the pod to execute in"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), - mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), - mcp.WithString("command", mcp.Description("Command to execute"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_execute_command", k8sTool.handleExecCommand))) - s.AddTool(mcp.NewTool("k8s_get_available_api_resources", mcp.WithDescription("Get available Kubernetes API resources"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_available_api_resources", k8sTool.handleGetAvailableAPIResources))) @@ -632,82 +592,6 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithDescription("Get cluster configuration details"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_cluster_configuration", k8sTool.handleGetClusterConfiguration))) - s.AddTool(mcp.NewTool("k8s_rollout", - mcp.WithDescription("Perform rollout operations on Kubernetes resources (history, pause, restart, resume, status, undo)"), - mcp.WithString("action", mcp.Description("The rollout action to perform"), mcp.Required()), - mcp.WithString("resource_type", mcp.Description("The type of resource to rollout (e.g., deployment)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource to rollout"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_rollout", k8sTool.handleRollout))) - - s.AddTool(mcp.NewTool("k8s_label_resource", - mcp.WithDescription("Add or update labels on a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("labels", mcp.Description("Space-separated key=value pairs for labels"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_label_resource", k8sTool.handleLabelResource))) - - s.AddTool(mcp.NewTool("k8s_annotate_resource", - mcp.WithDescription("Add or update annotations on a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("annotations", mcp.Description("Space-separated key=value pairs for annotations"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_annotate_resource", k8sTool.handleAnnotateResource))) - - s.AddTool(mcp.NewTool("k8s_remove_annotation", - mcp.WithDescription("Remove an annotation from a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("annotation_key", mcp.Description("The key of the annotation to remove"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_annotation", k8sTool.handleRemoveAnnotation))) - - s.AddTool(mcp.NewTool("k8s_remove_label", - mcp.WithDescription("Remove a label from a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("label_key", mcp.Description("The key of the label to remove"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_label", k8sTool.handleRemoveLabel))) - - s.AddTool(mcp.NewTool("k8s_create_resource", - mcp.WithDescription("Create a Kubernetes resource from YAML content"), - mcp.WithString("yaml_content", mcp.Description("YAML content of the resource"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - yamlContent := mcp.ParseString(request, "yaml_content", "") - - if yamlContent == "" { - return mcp.NewToolResultError("yaml_content is required"), nil - } - - // Create temporary file - tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(yamlContent); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil - } - tmpFile.Close() - - result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil - } - - return result, nil - }))) - - s.AddTool(mcp.NewTool("k8s_create_resource_from_url", - mcp.WithDescription("Create a Kubernetes resource from a URL pointing to a YAML manifest"), - mcp.WithString("url", mcp.Description("The URL of the manifest"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace to create the resource in")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource_from_url", k8sTool.handleCreateResourceFromURL))) - s.AddTool(mcp.NewTool("k8s_get_resource_yaml", mcp.WithDescription("Get the YAML representation of a Kubernetes resource"), mcp.WithString("resource_type", mcp.Description("Type of resource"), mcp.Required()), @@ -747,4 +631,124 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithString("resource_description", mcp.Description("Detailed description of the resource to generate"), mcp.Required()), mcp.WithString("resource_type", mcp.Description(fmt.Sprintf("Type of resource to generate (%s)", strings.Join(slices.Collect(resourceTypes), ", "))), mcp.Required()), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_generate_resource", k8sTool.handleGenerateResource))) + + // Write tools - only registered when write operations are enabled + if !readOnly { + s.AddTool(mcp.NewTool("k8s_scale", + mcp.WithDescription("Scale a Kubernetes deployment"), + mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), + mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_scale", k8sTool.handleScaleDeployment))) + + s.AddTool(mcp.NewTool("k8s_patch_resource", + mcp.WithDescription("Patch a Kubernetes resource using strategic merge patch"), + mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("patch", mcp.Description("JSON patch to apply"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) + + s.AddTool(mcp.NewTool("k8s_apply_manifest", + mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), + mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_apply_manifest", k8sTool.handleApplyManifest))) + + s.AddTool(mcp.NewTool("k8s_delete_resource", + mcp.WithDescription("Delete a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_delete_resource", k8sTool.handleDeleteResource))) + + s.AddTool(mcp.NewTool("k8s_check_service_connectivity", + mcp.WithDescription("Check connectivity to a service using a temporary curl pod"), + mcp.WithString("service_name", mcp.Description("Service name to test (e.g., my-service.my-namespace.svc.cluster.local:80)"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace to run the check from (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_check_service_connectivity", k8sTool.handleCheckServiceConnectivity))) + + s.AddTool(mcp.NewTool("k8s_execute_command", + mcp.WithDescription("Execute a command in a Kubernetes pod"), + mcp.WithString("pod_name", mcp.Description("Name of the pod to execute in"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), + mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), + mcp.WithString("command", mcp.Description("Command to execute"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_execute_command", k8sTool.handleExecCommand))) + + s.AddTool(mcp.NewTool("k8s_rollout", + mcp.WithDescription("Perform rollout operations on Kubernetes resources (history, pause, restart, resume, status, undo)"), + mcp.WithString("action", mcp.Description("The rollout action to perform"), mcp.Required()), + mcp.WithString("resource_type", mcp.Description("The type of resource to rollout (e.g., deployment)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource to rollout"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_rollout", k8sTool.handleRollout))) + + s.AddTool(mcp.NewTool("k8s_label_resource", + mcp.WithDescription("Add or update labels on a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("labels", mcp.Description("Space-separated key=value pairs for labels"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_label_resource", k8sTool.handleLabelResource))) + + s.AddTool(mcp.NewTool("k8s_annotate_resource", + mcp.WithDescription("Add or update annotations on a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("annotations", mcp.Description("Space-separated key=value pairs for annotations"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_annotate_resource", k8sTool.handleAnnotateResource))) + + s.AddTool(mcp.NewTool("k8s_remove_annotation", + mcp.WithDescription("Remove an annotation from a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("annotation_key", mcp.Description("The key of the annotation to remove"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_annotation", k8sTool.handleRemoveAnnotation))) + + s.AddTool(mcp.NewTool("k8s_remove_label", + mcp.WithDescription("Remove a label from a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("label_key", mcp.Description("The key of the label to remove"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_label", k8sTool.handleRemoveLabel))) + + s.AddTool(mcp.NewTool("k8s_create_resource", + mcp.WithDescription("Create a Kubernetes resource from YAML content"), + mcp.WithString("yaml_content", mcp.Description("YAML content of the resource"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + yamlContent := mcp.ParseString(request, "yaml_content", "") + + if yamlContent == "" { + return mcp.NewToolResultError("yaml_content is required"), nil + } + + // Create temporary file + tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(yamlContent); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil + } + tmpFile.Close() + + result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil + } + + return result, nil + }))) + + s.AddTool(mcp.NewTool("k8s_create_resource_from_url", + mcp.WithDescription("Create a Kubernetes resource from a URL pointing to a YAML manifest"), + mcp.WithString("url", mcp.Description("The URL of the manifest"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace to create the resource in")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource_from_url", k8sTool.handleCreateResourceFromURL))) + } }