Skip to content

Commit 4485bf4

Browse files
authored
feat(cli): add --read-only flag to disable write operations (#41)
* 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 <matteo.mori@rvu.co.uk> * fix: disable shell tool in read-only mode The utils provider exposes a `shell` tool that executes arbitrary commands, bypassing read-only restrictions. In read-only mode, this tool is now disabled. Also pass readOnly to all providers (kubescape, prometheus, utils) for consistency with the existing providers. Signed-off-by: Matteo Mori <matteo.mori@rvu.co.uk> --------- Signed-off-by: Matteo Mori <matteo.mori@rvu.co.uk>
1 parent f31fe9e commit 4485bf4

13 files changed

Lines changed: 419 additions & 367 deletions

File tree

cmd/main.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
tools []string
4040
kubeconfig *string
4141
showVersion bool
42+
readOnly bool
4243

4344
// These variables should be set during build time using -ldflags
4445
Name = "kagent-tools-server"
@@ -58,6 +59,7 @@ func init() {
5859
rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP")
5960
rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.")
6061
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit")
62+
rootCmd.Flags().BoolVar(&readOnly, "read-only", false, "Run in read-only mode (disable tools that perform write operations)")
6163
kubeconfig = rootCmd.Flags().String("kubeconfig", "", "kubeconfig file path (optional, defaults to in-cluster config)")
6264

6365
// if found .env file, load it
@@ -119,17 +121,21 @@ func run(cmd *cobra.Command, args []string) {
119121
attribute.Bool("server.stdio_mode", stdio),
120122
attribute.Int("server.port", port),
121123
attribute.StringSlice("server.tools", tools),
124+
attribute.Bool("server.read_only", readOnly),
122125
)
123126

124127
logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate)
128+
if readOnly {
129+
logger.Get().Info("Running in read-only mode - write operations are disabled")
130+
}
125131

126132
mcp := server.NewMCPServer(
127133
Name,
128134
Version,
129135
)
130136

131137
// Register tools
132-
registerMCP(mcp, tools, *kubeconfig)
138+
registerMCP(mcp, tools, *kubeconfig, readOnly)
133139

134140
// Create wait group for server goroutines
135141
var wg sync.WaitGroup
@@ -285,17 +291,17 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) {
285291
}
286292
}
287293

288-
func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) {
294+
func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) {
289295
// A map to hold tool providers and their registration functions
290296
toolProviderMap := map[string]func(*server.MCPServer){
291-
"argo": argo.RegisterTools,
292-
"cilium": cilium.RegisterTools,
293-
"helm": helm.RegisterTools,
294-
"istio": istio.RegisterTools,
295-
"k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) },
296-
"kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig) },
297-
"prometheus": prometheus.RegisterTools,
298-
"utils": utils.RegisterTools,
297+
"argo": func(s *server.MCPServer) { argo.RegisterTools(s, readOnly) },
298+
"cilium": func(s *server.MCPServer) { cilium.RegisterTools(s, readOnly) },
299+
"helm": func(s *server.MCPServer) { helm.RegisterTools(s, readOnly) },
300+
"istio": func(s *server.MCPServer) { istio.RegisterTools(s, readOnly) },
301+
"k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig, readOnly) },
302+
"kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig, readOnly) },
303+
"prometheus": func(s *server.MCPServer) { prometheus.RegisterTools(s, readOnly) },
304+
"utils": func(s *server.MCPServer) { utils.RegisterTools(s, readOnly) },
299305
}
300306

301307
// If no specific tools are specified, register all available tools.

pkg/argo/argo.go

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,8 @@ func handleListRollouts(ctx context.Context, request mcp.CallToolRequest) (*mcp.
375375
return mcp.NewToolResultText(output), nil
376376
}
377377

378-
func RegisterTools(s *server.MCPServer) {
378+
func RegisterTools(s *server.MCPServer, readOnly bool) {
379+
// Read-only tools - always registered
379380
s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install",
380381
mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"),
381382
mcp.WithString("namespace", mcp.Description("The namespace where Argo Rollouts is installed")),
@@ -392,36 +393,39 @@ func RegisterTools(s *server.MCPServer) {
392393
mcp.WithString("type", mcp.Description("What to list: rollouts or experiments"), mcp.DefaultString("rollouts")),
393394
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_rollouts_list", handleListRollouts)))
394395

395-
s.AddTool(mcp.NewTool("argo_promote_rollout",
396-
mcp.WithDescription("Promote a paused rollout to the next step"),
397-
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()),
398-
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
399-
mcp.WithString("full", mcp.Description("Promote the rollout to the final step")),
400-
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout)))
401-
402-
s.AddTool(mcp.NewTool("argo_pause_rollout",
403-
mcp.WithDescription("Pause a rollout"),
404-
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()),
405-
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
406-
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout)))
407-
408-
s.AddTool(mcp.NewTool("argo_set_rollout_image",
409-
mcp.WithDescription("Set the image of a rollout"),
410-
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()),
411-
mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()),
412-
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
413-
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage)))
414-
415-
s.AddTool(mcp.NewTool("argo_verify_gateway_plugin",
416-
mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"),
417-
mcp.WithString("version", mcp.Description("The version of the plugin to check")),
418-
mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")),
419-
mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")),
420-
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin)))
421-
422396
s.AddTool(mcp.NewTool("argo_check_plugin_logs",
423397
mcp.WithDescription("Check the logs of the Argo Rollouts Gateway API plugin"),
424398
mcp.WithString("namespace", mcp.Description("The namespace of the plugin resources")),
425399
mcp.WithString("timeout", mcp.Description("Timeout for log collection in seconds")),
426400
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_check_plugin_logs", handleCheckPluginLogs)))
401+
402+
// Write tools - only registered when not in read-only mode
403+
if !readOnly {
404+
s.AddTool(mcp.NewTool("argo_promote_rollout",
405+
mcp.WithDescription("Promote a paused rollout to the next step"),
406+
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()),
407+
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
408+
mcp.WithString("full", mcp.Description("Promote the rollout to the final step")),
409+
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout)))
410+
411+
s.AddTool(mcp.NewTool("argo_pause_rollout",
412+
mcp.WithDescription("Pause a rollout"),
413+
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()),
414+
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
415+
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout)))
416+
417+
s.AddTool(mcp.NewTool("argo_set_rollout_image",
418+
mcp.WithDescription("Set the image of a rollout"),
419+
mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()),
420+
mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()),
421+
mcp.WithString("namespace", mcp.Description("The namespace of the rollout")),
422+
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage)))
423+
424+
s.AddTool(mcp.NewTool("argo_verify_gateway_plugin",
425+
mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"),
426+
mcp.WithString("version", mcp.Description("The version of the plugin to check")),
427+
mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")),
428+
mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")),
429+
), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin)))
430+
}
427431
}

0 commit comments

Comments
 (0)