Repository: containers/kubernetes-mcp-server

Issue:
Adding namespace and tools control arguments
## Description

Adding an argument (like --namespace) to select a namespace to limit all commands to will be great as a security feature to limit the access to the mcp server to a single namespace even if the kubeconfig has access to multiple (or cluster level access).
Another argument that will help with access control is one to limit tools that can be used with the mcp server (like --exclude-tools or --include-tools). This argument will limit the tools the mcp server can access (like --read-only) but it will enable each user to choose which tools to expose.

My use case is that I want to create an API that will allow users to send questions about their namespaces and will return an answer. The namespace is checked with their permissions but the can ask in the question body to get another namespace's resources. Another thing is I would want to exclude the configuration_view tool as well add the --read-only flag.

Hints from issue comments:
@ardaguclu: @N0YU0 I think this new profile proposal https://github.com/manusa/kubernetes-mcp-server/pull/134 would mitigate some of your concerns (i.e. excluding `configuration_view` tool).

After thinking on this, maybe instead of adding a new profile that are mostly a set of static rules per each requirement. Maybe we can provide a `custom` profile that is fully up to the passed configuration file.
* included tools list
* namespace (default all)
* denied resources

@manusa WDYT?@manusa: We need to think about it.

It might be interesting to deal with all this in the scope of configuration files #131 + #137

If we check the GitHub MCP server, they introduced the concept of [_Dynamic tools_](https://github.com/github/github-mcp-server/issues/189) ([implementation](https://github.com/github/github-mcp-server/pull/188)) which is very similar to some of the features we want to tackle with **profiles** and **configuration file**.

My initial idea would be to create a set of well-defined tool groups for example:
- resources_readonly
- resources_destructive
- pods_readonly
- pods_destructive
- helm_readonly
- ...

The `StaticConfig` can then have a field toolSets which defines the tools available.
In addition, and in scope of #137, every other configuration flag should be settable via the config file too. ---> StaticConfig is the single source of truth for the MCP Server configuration+behavior.

Finally, continuing on #134, we can provide a set of predefined profiles with defaults (full, full-safe) for all values.

Still (see #137), **some** cmd flags are preserved for one-off overrides.

Config inheritance/extension should also be taken into account. i.e. a user wants to have the `full` behavior, but just limit the namespace.
@ardaguclu: @N0YU0 allow/deny tool list makes sense.  However, after thinking on this, I'm not sure I fully understand the namespace part.

Namespace is dynamic and changing based on each user. So that statically defining namespace in the config file of MCP Server will limit the entire MCP Server to a set of namespaces. Is this similar to what you proposed?.@N0YU0: @ardaguclu The namespace flag that I proposed was more so that you could define the scoped namespace that the mcp server could access.
I'm not sure if this would be a good addition to the configuration file, but as a cli flag it would help me immensely.
I use the mcp server in stdio mode every time a request is received on my server. For security purposes I would like to limit the mcp server that is running per request with the namespace given in the request (instead of trying to detect prompt injection of another namespace in the request body). I'm not sure if there is a better way to limit the call to the mcp for a specific namespace.@ardaguclu: What about non-namespaced resources?@N0YU0: I think ignoring it with cluster scoped resources, like the behavior of kubectl with --namespace flag with cluster scoped resources (e.g. kubectl get --namespace foo nodes) would be fine. @ardaguclu: @manusa so we don't need namespace based restriction? or we'll handle it in a different issue?.@manusa: > [@manusa](https://github.com/manusa) so we don't need namespace based restriction? or we'll handle it in a different issue?.

Sorry, I missed this comment.

Do we still want/need to provide the namespace restriction?@ardaguclu: > > [@manusa](https://github.com/manusa) so we don't need namespace based restriction? or we'll handle it in a different issue?.
> 
> Sorry, I missed this comment.
> 
> Do we still want/need to provide the namespace restriction?

In my opinion, namespace restriction can be achieved by relying on RBAC rules in the cluster. If cluster admin correctly restricts access of the users to other namespaces, API server will automatically reject the requests already. We don't need to add such restriction in MCP Server.@manusa: > In my opinion, namespace restriction can be achieved by relying on RBAC rules in the cluster. If cluster admin correctly restricts access of the users to other namespaces, API server will automatically reject the requests already. We don't need to add such restriction in MCP Server.

👍 This is precisely how I see it.
By adding _client-side_ restrictions we're essentially duplicating the logic of RBAC and other Kubernetes components (with the maintenance burden this implies).

Once we have the entire OAuth flow working, we should document some of the common scenarios and how to achieve them.
@ardaguclu: > Once we have the entire OAuth flow working, we should document some of the common scenarios and how to achieve them.

Fully agree. Documenting how hardening the security, recommended configurations, etc.

Solution patch:
diff --git a/.gitignore b/.gitignore
index 1d4fb59..8b36a25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .idea/
+.vscode/
 .docusaurus/
 node_modules/
 
diff --git a/pkg/config/config.go b/pkg/config/config.go
index f6471ea..af3f0ae 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -8,6 +8,15 @@ import (
 
 type StaticConfig struct {
 	DeniedResources []GroupVersionKind `toml:"denied_resources"`
+
+	LogLevel           int    `toml:"log_level,omitempty"`
+	SSEPort            int    `toml:"sse_port,omitempty"`
+	HTTPPort           int    `toml:"http_port,omitempty"`
+	SSEBaseURL         string `toml:"sse_base_url,omitempty"`
+	KubeConfig         string `toml:"kubeconfig,omitempty"`
+	ListOutput         string `toml:"list_output,omitempty"`
+	ReadOnly           bool   `toml:"read_only,omitempty"`
+	DisableDestructive bool   `toml:"disable_destructive,omitempty"`
 }
 
 type GroupVersionKind struct {
diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go
index e59b7c2..9d36fe3 100644
--- a/pkg/kubernetes-mcp-server/cmd/root.go
+++ b/pkg/kubernetes-mcp-server/cmd/root.go
@@ -62,9 +62,10 @@ type MCPServerOptions struct {
 
 func NewMCPServerOptions(streams genericiooptions.IOStreams) *MCPServerOptions {
 	return &MCPServerOptions{
-		IOStreams:  streams,
-		Profile:    "full",
-		ListOutput: "table",
+		IOStreams:    streams,
+		Profile:      "full",
+		ListOutput:   "table",
+		StaticConfig: &config.StaticConfig{},
 	}
 }
 
@@ -76,7 +77,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
 		Long:    long,
 		Example: examples,
 		RunE: func(c *cobra.Command, args []string) error {
-			if err := o.Complete(); err != nil {
+			if err := o.Complete(c); err != nil {
 				return err
 			}
 			if err := o.Validate(); err != nil {
@@ -98,16 +99,14 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
 	cmd.Flags().StringVar(&o.SSEBaseUrl, "sse-base-url", o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
 	cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
 	cmd.Flags().StringVar(&o.Profile, "profile", o.Profile, "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
-	cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+")")
+	cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
 	cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
 	cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
 
 	return cmd
 }
 
-func (m *MCPServerOptions) Complete() error {
-	m.initializeLogging()
-
+func (m *MCPServerOptions) Complete(cmd *cobra.Command) error {
 	if m.ConfigPath != "" {
 		cnf, err := config.ReadConfig(m.ConfigPath)
 		if err != nil {
@@ -116,16 +115,47 @@ func (m *MCPServerOptions) Complete() error {
 		m.StaticConfig = cnf
 	}
 
+	m.loadFlags(cmd)
+
+	m.initializeLogging()
+
 	return nil
 }
 
+func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
+	if cmd.Flag("log-level").Changed {
+		m.StaticConfig.LogLevel = m.LogLevel
+	}
+	if cmd.Flag("sse-port").Changed {
+		m.StaticConfig.SSEPort = m.SSEPort
+	}
+	if cmd.Flag("http-port").Changed {
+		m.StaticConfig.HTTPPort = m.HttpPort
+	}
+	if cmd.Flag("sse-base-url").Changed {
+		m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
+	}
+	if cmd.Flag("kubeconfig").Changed {
+		m.StaticConfig.KubeConfig = m.Kubeconfig
+	}
+	if cmd.Flag("list-output").Changed || m.StaticConfig.ListOutput == "" {
+		m.StaticConfig.ListOutput = m.ListOutput
+	}
+	if cmd.Flag("read-only").Changed {
+		m.StaticConfig.ReadOnly = m.ReadOnly
+	}
+	if cmd.Flag("disable-destructive").Changed {
+		m.StaticConfig.DisableDestructive = m.DisableDestructive
+	}
+}
+
 func (m *MCPServerOptions) initializeLogging() {
 	flagSet := flag.NewFlagSet("klog", flag.ContinueOnError)
 	klog.InitFlags(flagSet)
 	loggerOptions := []textlogger.ConfigOption{textlogger.Output(m.Out)}
-	if m.LogLevel >= 0 {
-		loggerOptions = append(loggerOptions, textlogger.Verbosity(m.LogLevel))
-		_ = flagSet.Parse([]string{"--v", strconv.Itoa(m.LogLevel)})
+	if m.StaticConfig.LogLevel >= 0 {
+		loggerOptions = append(loggerOptions, textlogger.Verbosity(m.StaticConfig.LogLevel))
+		_ = flagSet.Parse([]string{"--v", strconv.Itoa(m.StaticConfig.LogLevel)})
 	}
 	logger := textlogger.NewLogger(textlogger.NewConfig(loggerOptions...))
 	klog.SetLoggerWithOptions(logger)
@@ -140,16 +170,16 @@ func (m *MCPServerOptions) Run() error {
 	if profile == nil {
 		return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", "))
 	}
-	listOutput := output.FromString(m.ListOutput)
+	listOutput := output.FromString(m.StaticConfig.ListOutput)
 	if listOutput == nil {
-		return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.ListOutput, strings.Join(output.Names, ", "))
+		return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.StaticConfig.ListOutput, strings.Join(output.Names, ", "))
 	}
 	klog.V(1).Info("Starting kubernetes-mcp-server")
 	klog.V(1).Infof(" - Config: %s", m.ConfigPath)
 	klog.V(1).Infof(" - Profile: %s", profile.GetName())
 	klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName())
-	klog.V(1).Infof(" - Read-only mode: %t", m.ReadOnly)
-	klog.V(1).Infof(" - Disable destructive tools: %t", m.DisableDestructive)
+	klog.V(1).Infof(" - Read-only mode: %t", m.StaticConfig.ReadOnly)
+	klog.V(1).Infof(" - Disable destructive tools: %t", m.StaticConfig.DisableDestructive)
 
 	if m.Version {
 		_, _ = fmt.Fprintf(m.Out, "%s\n", version.Version)
@@ -158,9 +188,9 @@ func (m *MCPServerOptions) Run() error {
 	mcpServer, err := mcp.NewServer(mcp.Configuration{
 		Profile:            profile,
 		ListOutput:         listOutput,
-		ReadOnly:           m.ReadOnly,
-		DisableDestructive: m.DisableDestructive,
-		Kubeconfig:         m.Kubeconfig,
+		ReadOnly:           m.StaticConfig.ReadOnly,
+		DisableDestructive: m.StaticConfig.DisableDestructive,
+		Kubeconfig:         m.StaticConfig.KubeConfig,
 		StaticConfig:       m.StaticConfig,
 	})
 	if err != nil {
@@ -170,19 +200,19 @@ func (m *MCPServerOptions) Run() error {
 
 	ctx := context.Background()
 
-	if m.SSEPort > 0 {
-		sseServer := mcpServer.ServeSse(m.SSEBaseUrl)
+	if m.StaticConfig.SSEPort > 0 {
+		sseServer := mcpServer.ServeSse(m.StaticConfig.SSEBaseURL)
 		defer func() { _ = sseServer.Shutdown(ctx) }()
-		klog.V(0).Infof("SSE server starting on port %d and path /sse", m.SSEPort)
-		if err := sseServer.Start(fmt.Sprintf(":%d", m.SSEPort)); err != nil {
+		klog.V(0).Infof("SSE server starting on port %d and path /sse", m.StaticConfig.SSEPort)
+		if err := sseServer.Start(fmt.Sprintf(":%d", m.StaticConfig.SSEPort)); err != nil {
 			return fmt.Errorf("failed to start SSE server: %w\n", err)
 		}
 	}
 
-	if m.HttpPort > 0 {
+	if m.StaticConfig.HTTPPort > 0 {
 		httpServer := mcpServer.ServeHTTP()
-		klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.HttpPort)
-		if err := httpServer.Start(fmt.Sprintf(":%d", m.HttpPort)); err != nil {
+		klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.StaticConfig.HTTPPort)
+		if err := httpServer.Start(fmt.Sprintf(":%d", m.StaticConfig.HTTPPort)); err != nil {
 			return fmt.Errorf("failed to start streaming HTTP server: %w\n", err)
 		}
 	}
diff --git a/pkg/kubernetes/accesscontrol.go b/pkg/kubernetes/accesscontrol.go
new file mode 100644
index 0000000..fa9a002
--- /dev/null
+++ b/pkg/kubernetes/accesscontrol.go
@@ -0,0 +1,40 @@
+package kubernetes
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/manusa/kubernetes-mcp-server/pkg/config"
+)
+
+// isAllowed checks the resource is in denied list or not.
+// If it is in denied list, this function returns false.
+func isAllowed(
+	staticConfig *config.StaticConfig, // TODO: maybe just use the denied resource slice
+	gvk *schema.GroupVersionKind,
+) bool {
+	if staticConfig == nil {
+		return true
+	}
+
+	for _, val := range staticConfig.DeniedResources {
+		// If kind is empty, that means Group/Version pair is denied entirely
+		if val.Kind == "" {
+			if gvk.Group == val.Group && gvk.Version == val.Version {
+				return false
+			}
+		}
+		if gvk.Group == val.Group &&
+			gvk.Version == val.Version &&
+			gvk.Kind == val.Kind {
+			return false
+		}
+	}
+
+	return true
+}
+
+func isNotAllowedError(gvk *schema.GroupVersionKind) error {
+	return fmt.Errorf("resource not allowed: %s", gvk.String())
+}
diff --git a/pkg/kubernetes/accesscontrol_clientset.go b/pkg/kubernetes/accesscontrol_clientset.go
new file mode 100644
index 0000000..f5c06ff
--- /dev/null
+++ b/pkg/kubernetes/accesscontrol_clientset.go
@@ -0,0 +1,130 @@
+package kubernetes
+
+import (
+	"context"
+	"fmt"
+
+	authorizationv1api "k8s.io/api/authorization/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/kubernetes"
+	authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
+	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/remotecommand"
+	"k8s.io/metrics/pkg/apis/metrics"
+	metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
+	metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
+
+	"github.com/manusa/kubernetes-mcp-server/pkg/config"
+)
+
+// AccessControlClientset is a limited clientset delegating interface to the standard kubernetes.Clientset
+// Only a limited set of functions are implemented with a single point of access to the kubernetes API where
+// apiVersion and kinds are checked for allowed access
+type AccessControlClientset struct {
+	cfg             *rest.Config
+	delegate        kubernetes.Interface
+	discoveryClient discovery.DiscoveryInterface
+	metricsV1beta1  *metricsv1beta1.MetricsV1beta1Client
+	staticConfig    *config.StaticConfig // TODO: maybe just store the denied resource slice
+}
+
+func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface {
+	return a.discoveryClient
+}
+
+func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) {
+	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
+	if !isAllowed(a.staticConfig, gvk) {
+		return nil, isNotAllowedError(gvk)
+	}
+	return a.delegate.CoreV1().Pods(namespace), nil
+}
+
+func (a *AccessControlClientset) PodsExec(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
+	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
+	if !isAllowed(a.staticConfig, gvk) {
+		return nil, isNotAllowedError(gvk)
+	}
+	// Compute URL
+	// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397
+	execRequest := a.delegate.CoreV1().RESTClient().
+		Post().
+		Resource("pods").
+		Namespace(namespace).
+		Name(name).
+		SubResource("exec")
+	execRequest.VersionedParams(podExecOptions, ParameterCodec)
+	spdyExec, err := remotecommand.NewSPDYExecutor(a.cfg, "POST", execRequest.URL())
+	if err != nil {
+		return nil, err
+	}
+	webSocketExec, err := remotecommand.NewWebSocketExecutor(a.cfg, "GET", execRequest.URL().String())
+	if err != nil {
+		return nil, err
+	}
+	return remotecommand.NewFallbackExecutor(webSocketExec, spdyExec, func(err error) bool {
+		return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
+	})
+}
+
+func (a *AccessControlClientset) PodsMetricses(ctx context.Context, namespace, name string, listOptions metav1.ListOptions) (*metrics.PodMetricsList, error) {
+	gvk := &schema.GroupVersionKind{Group: metrics.GroupName, Version: metricsv1beta1api.SchemeGroupVersion.Version, Kind: "PodMetrics"}
+	if !isAllowed(a.staticConfig, gvk) {
+		return nil, isNotAllowedError(gvk)
+	}
+	versionedMetrics := &metricsv1beta1api.PodMetricsList{}
+	var err error
+	if name != "" {
+		m, err := a.metricsV1beta1.PodMetricses(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, name, err)
+		}
+		versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
+	} else {
+		versionedMetrics, err = a.metricsV1beta1.PodMetricses(namespace).List(ctx, listOptions)
+		if err != nil {
+			return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
+		}
+	}
+	convertedMetrics := &metrics.PodMetricsList{}
+	return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
+}
+
+func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) {
+	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}
+	if !isAllowed(a.staticConfig, gvk) {
+		return nil, isNotAllowedError(gvk)
+	}
+	return a.delegate.CoreV1().Services(namespace), nil
+}
+
+func (a *AccessControlClientset) SelfSubjectAccessReviews() (authorizationv1.SelfSubjectAccessReviewInterface, error) {
+	gvk := &schema.GroupVersionKind{Group: authorizationv1api.GroupName, Version: authorizationv1api.SchemeGroupVersion.Version, Kind: "SelfSubjectAccessReview"}
+	if !isAllowed(a.staticConfig, gvk) {
+		return nil, isNotAllowedError(gvk)
+	}
+	return a.delegate.AuthorizationV1().SelfSubjectAccessReviews(), nil
+}
+
+func NewAccessControlClientset(cfg *rest.Config, staticConfig *config.StaticConfig) (*AccessControlClientset, error) {
+	clientSet, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+	metricsClient, err := metricsv1beta1.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return &AccessControlClientset{
+		cfg:             cfg,
+		delegate:        clientSet,
+		discoveryClient: clientSet.DiscoveryClient,
+		metricsV1beta1:  metricsClient,
+		staticConfig:    staticConfig,
+	}, nil
+}
diff --git a/pkg/kubernetes/accesscontrol_restmapper.go b/pkg/kubernetes/accesscontrol_restmapper.go
new file mode 100644
index 0000000..a821717
--- /dev/null
+++ b/pkg/kubernetes/accesscontrol_restmapper.go
@@ -0,0 +1,80 @@
+package kubernetes
+
+import (
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/restmapper"
+
+	"github.com/manusa/kubernetes-mcp-server/pkg/config"
+)
+
+type AccessControlRESTMapper struct {
+	delegate     *restmapper.DeferredDiscoveryRESTMapper
+	staticConfig *config.StaticConfig // TODO: maybe just store the denied resource slice
+}
+
+var _ meta.RESTMapper = &AccessControlRESTMapper{}
+
+func (a AccessControlRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
+	gvk, err := a.delegate.KindFor(resource)
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+	if !isAllowed(a.staticConfig, &gvk) {
+		return schema.GroupVersionKind{}, isNotAllowedError(&gvk)
+	}
+	return gvk, nil
+}
+
+func (a AccessControlRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
+	gvks, err := a.delegate.KindsFor(resource)
+	if err != nil {
+		return nil, err
+	}
+	for i := range gvks {
+		if !isAllowed(a.staticConfig, &gvks[i]) {
+			return nil, isNotAllowedError(&gvks[i])
+		}
+	}
+	return gvks, nil
+}
+
+func (a AccessControlRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
+	return a.delegate.ResourceFor(input)
+}
+
+func (a AccessControlRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
+	return a.delegate.ResourcesFor(input)
+}
+
+func (a AccessControlRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
+	for _, version := range versions {
+		gvk := &schema.GroupVersionKind{Group: gk.Group, Version: version, Kind: gk.Kind}
+		if !isAllowed(a.staticConfig, gvk) {
+			return nil, isNotAllowedError(gvk)
+		}
+	}
+	return a.delegate.RESTMapping(gk, versions...)
+}
+
+func (a AccessControlRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
+	for _, version := range versions {
+		gvk := &schema.GroupVersionKind{Group: gk.Group, Version: version, Kind: gk.Kind}
+		if !isAllowed(a.staticConfig, gvk) {
+			return nil, isNotAllowedError(gvk)
+		}
+	}
+	return a.delegate.RESTMappings(gk, versions...)
+}
+
+func (a AccessControlRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
+	return a.delegate.ResourceSingularizer(resource)
+}
+
+func (a AccessControlRESTMapper) Reset() {
+	a.delegate.Reset()
+}
+
+func NewAccessControlRESTMapper(delegate *restmapper.DeferredDiscoveryRESTMapper, staticConfig *config.StaticConfig) *AccessControlRESTMapper {
+	return &AccessControlRESTMapper{delegate: delegate, staticConfig: staticConfig}
+}
diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go
index e44f92f..7ed808c 100644
--- a/pkg/kubernetes/kubernetes.go
+++ b/pkg/kubernetes/kubernetes.go
@@ -2,17 +2,16 @@ package kubernetes
 
 import (
 	"context"
+	"k8s.io/apimachinery/pkg/runtime"
 	"strings"
 
 	"github.com/fsnotify/fsnotify"
 
-	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/discovery/cached/memory"
 	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/restmapper"
 	"k8s.io/client-go/tools/clientcmd"
@@ -37,24 +36,27 @@ type Kubernetes struct {
 
 type Manager struct {
 	// Kubeconfig path override
-	Kubeconfig                  string
-	cfg                         *rest.Config
-	clientCmdConfig             clientcmd.ClientConfig
-	CloseWatchKubeConfig        CloseWatchKubeConfig
-	scheme                      *runtime.Scheme
-	parameterCodec              runtime.ParameterCodec
-	clientSet                   kubernetes.Interface
-	discoveryClient             discovery.CachedDiscoveryInterface
-	deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
-	dynamicClient               *dynamic.DynamicClient
-
-	StaticConfig *config.StaticConfig
+	Kubeconfig              string
+	cfg                     *rest.Config
+	clientCmdConfig         clientcmd.ClientConfig
+	discoveryClient         discovery.CachedDiscoveryInterface
+	accessControlClientSet  *AccessControlClientset
+	accessControlRESTMapper *AccessControlRESTMapper
+	dynamicClient           *dynamic.DynamicClient
+
+	staticConfig         *config.StaticConfig
+	CloseWatchKubeConfig CloseWatchKubeConfig
 }
 
+var Scheme = scheme.Scheme
+var ParameterCodec = runtime.NewParameterCodec(Scheme)
+
+var _ helm.Kubernetes = &Manager{}
+
 func NewManager(kubeconfig string, config *config.StaticConfig) (*Manager, error) {
 	k8s := &Manager{
 		Kubeconfig:   kubeconfig,
-		StaticConfig: config,
+		staticConfig: config,
 	}
 	if err := resolveKubernetesConfigurations(k8s); err != nil {
 		return nil, err
@@ -64,21 +66,19 @@ func NewManager(kubeconfig string, config *config.StaticConfig) (*Manager, error
 	//	return &impersonateRoundTripper{original}
 	//})
 	var err error
-	k8s.clientSet, err = kubernetes.NewForConfig(k8s.cfg)
+	k8s.accessControlClientSet, err = NewAccessControlClientset(k8s.cfg, k8s.staticConfig)
 	if err != nil {
 		return nil, err
 	}
-	k8s.discoveryClient = memory.NewMemCacheClient(discovery.NewDiscoveryClient(k8s.clientSet.CoreV1().RESTClient()))
-	k8s.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(k8s.discoveryClient)
+	k8s.discoveryClient = memory.NewMemCacheClient(k8s.accessControlClientSet.DiscoveryClient())
+	k8s.accessControlRESTMapper = NewAccessControlRESTMapper(
+		restmapper.NewDeferredDiscoveryRESTMapper(k8s.discoveryClient),
+		k8s.staticConfig,
+	)
 	k8s.dynamicClient, err = dynamic.NewForConfig(k8s.cfg)
 	if err != nil {
 		return nil, err
 	}
-	k8s.scheme = runtime.NewScheme()
-	if err = v1.AddToScheme(k8s.scheme); err != nil {
-		return nil, err
-	}
-	k8s.parameterCodec = runtime.NewParameterCodec(k8s.scheme)
 	return k8s, nil
 }
 
@@ -129,7 +129,7 @@ func (m *Manager) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error
 }
 
 func (m *Manager) ToRESTMapper() (meta.RESTMapper, error) {
-	return m.deferredDiscoveryRESTMapper, nil
+	return m.accessControlRESTMapper, nil
 }
 
 func (m *Manager) Derived(ctx context.Context) *Kubernetes {
@@ -156,15 +156,16 @@ func (m *Manager) Derived(ctx context.Context) *Kubernetes {
 		Kubeconfig:      m.Kubeconfig,
 		clientCmdConfig: clientcmd.NewDefaultClientConfig(clientCmdApiConfig, nil),
 		cfg:             derivedCfg,
-		scheme:          m.scheme,
-		parameterCodec:  m.parameterCodec,
 	}}
-	derived.manager.clientSet, err = kubernetes.NewForConfig(derived.manager.cfg)
+	derived.manager.accessControlClientSet, err = NewAccessControlClientset(derived.manager.cfg, derived.manager.staticConfig)
 	if err != nil {
 		return &Kubernetes{manager: m}
 	}
-	derived.manager.discoveryClient = memory.NewMemCacheClient(discovery.NewDiscoveryClient(derived.manager.clientSet.CoreV1().RESTClient()))
-	derived.manager.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(derived.manager.discoveryClient)
+	derived.manager.discoveryClient = memory.NewMemCacheClient(derived.manager.accessControlClientSet.DiscoveryClient())
+	derived.manager.accessControlRESTMapper = NewAccessControlRESTMapper(
+		restmapper.NewDeferredDiscoveryRESTMapper(derived.manager.discoveryClient),
+		derived.manager.staticConfig,
+	)
 	derived.manager.dynamicClient, err = dynamic.NewForConfig(derived.manager.cfg)
 	if err != nil {
 		return &Kubernetes{manager: m}
diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go
index f5a96c9..1c7e704 100644
--- a/pkg/kubernetes/pods.go
+++ b/pkg/kubernetes/pods.go
@@ -5,21 +5,20 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"k8s.io/metrics/pkg/apis/metrics"
-	metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
-	metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned"
 
-	"github.com/manusa/kubernetes-mcp-server/pkg/version"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	labelutil "k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/util/httpstream"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/client-go/tools/remotecommand"
+	"k8s.io/metrics/pkg/apis/metrics"
+	metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
+
+	"github.com/manusa/kubernetes-mcp-server/pkg/version"
 )
 
 type PodsTopOptions struct {
@@ -49,7 +48,7 @@ func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (*unst
 
 func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
 	namespace = k.NamespaceOrDefault(namespace)
-	pod, err := k.manager.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
+	pod, err := k.ResourcesGet(ctx, &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, namespace, name)
 	if err != nil {
 		return "", err
 	}
@@ -62,11 +61,15 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
 
 	// Delete managed service
 	if isManaged {
-		if sl, _ := k.manager.clientSet.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{
+		services, err := k.manager.accessControlClientSet.Services(namespace)
+		if err != nil {
+			return "", err
+		}
+		if sl, _ := services.List(ctx, metav1.ListOptions{
 			LabelSelector: managedLabelSelector.String(),
 		}); sl != nil {
 			for _, svc := range sl.Items {
-				_ = k.manager.clientSet.CoreV1().Services(namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{})
+				_ = services.Delete(ctx, svc.Name, metav1.DeleteOptions{})
 			}
 		}
 	}
@@ -86,12 +89,16 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
 
 	}
 	return "Pod deleted successfully",
-		k.manager.clientSet.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{})
+		k.ResourcesDelete(ctx, &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, namespace, name)
 }
 
 func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
 	tailLines := int64(256)
-	req := k.manager.clientSet.CoreV1().Pods(k.NamespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
+	pods, err := k.manager.accessControlClientSet.Pods(k.NamespaceOrDefault(namespace))
+	if err != nil {
+		return "", err
+	}
+	req := pods.GetLogs(name, &v1.PodLogOptions{
 		TailLines: &tailLines,
 		Container: container,
 	})
@@ -197,30 +204,16 @@ func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metr
 	} else {
 		namespace = k.NamespaceOrDefault(namespace)
 	}
-	metricsClient, err := metricsclientset.NewForConfig(k.manager.cfg)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create metrics client: %w", err)
-	}
-	versionedMetrics := &metricsv1beta1api.PodMetricsList{}
-	if options.Name != "" {
-		m, err := metricsClient.MetricsV1beta1().PodMetricses(namespace).Get(ctx, options.Name, metav1.GetOptions{})
-		if err != nil {
-			return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, options.Name, err)
-		}
-		versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
-	} else {
-		versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, options.ListOptions)
-		if err != nil {
-			return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
-		}
-	}
-	convertedMetrics := &metrics.PodMetricsList{}
-	return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
+	return k.manager.accessControlClientSet.PodsMetricses(ctx, namespace, options.Name, options.ListOptions)
 }
 
 func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
 	namespace = k.NamespaceOrDefault(namespace)
-	pod, err := k.manager.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
+	pods, err := k.manager.accessControlClientSet.Pods(namespace)
+	if err != nil {
+		return "", err
+	}
+	pod, err := pods.Get(ctx, name, metav1.GetOptions{})
 	if err != nil {
 		return "", err
 	}
@@ -237,7 +230,7 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
 		Stdout:    true,
 		Stderr:    true,
 	}
-	executor, err := k.createExecutor(namespace, name, podExecOptions)
+	executor, err := k.manager.accessControlClientSet.PodsExec(namespace, name, podExecOptions)
 	if err != nil {
 		return "", err
 	}
@@ -256,26 +249,3 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
 	}
 	return "", nil
 }
-
-func (k *Kubernetes) createExecutor(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
-	// Compute URL
-	// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397
-	req := k.manager.clientSet.CoreV1().RESTClient().
-		Post().
-		Resource("pods").
-		Namespace(namespace).
-		Name(name).
-		SubResource("exec")
-	req.VersionedParams(podExecOptions, k.manager.parameterCodec)
-	spdyExec, err := remotecommand.NewSPDYExecutor(k.manager.cfg, "POST", req.URL())
-	if err != nil {
-		return nil, err
-	}
-	webSocketExec, err := remotecommand.NewWebSocketExecutor(k.manager.cfg, "GET", req.URL().String())
-	if err != nil {
-		return nil, err
-	}
-	return remotecommand.NewFallbackExecutor(webSocketExec, spdyExec, func(err error) bool {
-		return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
-	})
-}
diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go
index efd08f7..ea38a9b 100644
--- a/pkg/kubernetes/resources.go
+++ b/pkg/kubernetes/resources.go
@@ -34,10 +34,6 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
 		return nil, err
 	}
 
-	if !k.isAllowed(gvk) {
-		return nil, fmt.Errorf("resource not allowed: %s", gvk.String())
-	}
-
 	// Check if operation is allowed for all namespaces (applicable for namespaced resources)
 	isNamespaced, _ := k.isNamespaced(gvk)
 	if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
@@ -55,10 +51,6 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK
 		return nil, err
 	}
 
-	if !k.isAllowed(gvk) {
-		return nil, fmt.Errorf("resource not allowed: %s", gvk.String())
-	}
-
 	// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
 	if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
 		namespace = k.NamespaceOrDefault(namespace)
@@ -86,10 +78,6 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
 		return err
 	}
 
-	if !k.isAllowed(gvk) {
-		return fmt.Errorf("resource not allowed: %s", gvk.String())
-	}
-
 	// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
 	if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
 		namespace = k.NamespaceOrDefault(namespace)
@@ -113,7 +101,7 @@ func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.Group
 	}
 	url = append(url, gvr.Resource)
 	var table metav1.Table
-	err := k.manager.clientSet.CoreV1().RESTClient().
+	err := k.manager.discoveryClient.RESTClient().
 		Get().
 		SetHeader("Accept", strings.Join([]string{
 			fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
@@ -121,7 +109,7 @@ func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.Group
 			"application/json",
 		}, ",")).
 		AbsPath(url...).
-		SpecificallyVersionedParams(&options.ListOptions, k.manager.parameterCodec, schema.GroupVersion{Version: "v1"}).
+		SpecificallyVersionedParams(&options.ListOptions, ParameterCodec, schema.GroupVersion{Version: "v1"}).
 		Do(ctx).Into(&table)
 	if err != nil {
 		return nil, err
@@ -152,10 +140,6 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u
 			return nil, rErr
 		}
 
-		if !k.isAllowed(&gvk) {
-			return nil, fmt.Errorf("resource not allowed: %s", gvk.String())
-		}
-
 		namespace := obj.GetNamespace()
 		// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
 		if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced {
@@ -169,44 +153,20 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u
 		}
 		// Clear the cache to ensure the next operation is performed on the latest exposed APIs (will change after the CRD creation)
 		if gvk.Kind == "CustomResourceDefinition" {
-			k.manager.deferredDiscoveryRESTMapper.Reset()
+			k.manager.accessControlRESTMapper.Reset()
 		}
 	}
 	return resources, nil
 }
 
 func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVersionResource, error) {
-	m, err := k.manager.deferredDiscoveryRESTMapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
+	m, err := k.manager.accessControlRESTMapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
 	if err != nil {
 		return nil, err
 	}
 	return &m.Resource, nil
 }
 
-// isAllowed checks the resource is in denied list or not.
-// If it is in denied list, this function returns false.
-func (k *Kubernetes) isAllowed(gvk *schema.GroupVersionKind) bool {
-	if k.manager.StaticConfig == nil {
-		return true
-	}
-
-	for _, val := range k.manager.StaticConfig.DeniedResources {
-		// If kind is empty, that means Group/Version pair is denied entirely
-		if val.Kind == "" {
-			if gvk.Group == val.Group && gvk.Version == val.Version {
-				return false
-			}
-		}
-		if gvk.Group == val.Group &&
-			gvk.Version == val.Version &&
-			gvk.Kind == val.Kind {
-			return false
-		}
-	}
-
-	return true
-}
-
 func (k *Kubernetes) isNamespaced(gvk *schema.GroupVersionKind) (bool, error) {
 	apiResourceList, err := k.manager.discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
 	if err != nil {
@@ -228,7 +188,11 @@ func (k *Kubernetes) supportsGroupVersion(groupVersion string) bool {
 }
 
 func (k *Kubernetes) canIUse(ctx context.Context, gvr *schema.GroupVersionResource, namespace, verb string) bool {
-	response, err := k.manager.clientSet.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, &authv1.SelfSubjectAccessReview{
+	accessReviews, err := k.manager.accessControlClientSet.SelfSubjectAccessReviews()
+	if err != nil {
+		return false
+	}
+	response, err := accessReviews.Create(ctx, &authv1.SelfSubjectAccessReview{
 		Spec: authv1.SelfSubjectAccessReviewSpec{ResourceAttributes: &authv1.ResourceAttributes{
 			Namespace: namespace,
 			Verb:      verb,



Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index bf862ef..1aac25f 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -50,6 +50,13 @@ kind = "Role
 
 func TestReadConfigValid(t *testing.T) {
 	validConfigPath := writeConfig(t, `
+log_level = 1
+sse_port = 9999
+kubeconfig = "test"
+list_output = "yaml"
+read_only = true
+disable_destructive = false
+
 [[denied_resources]]
 group = "apps"
 version = "v1"
@@ -78,6 +85,30 @@ version = "v1"
 			config.DeniedResources[0].Kind != "Deployment" {
 			t.Errorf("Unexpected denied resources: %v", config.DeniedResources[0])
 		}
+		if config.LogLevel != 1 {
+			t.Fatalf("Unexpected log level: %v", config.LogLevel)
+		}
+		if config.SSEPort != 9999 {
+			t.Fatalf("Unexpected sse_port value: %v", config.SSEPort)
+		}
+		if config.SSEBaseURL != "" {
+			t.Fatalf("Unexpected sse_base_url value: %v", config.SSEBaseURL)
+		}
+		if config.HTTPPort != 0 {
+			t.Fatalf("Unexpected http_port value: %v", config.HTTPPort)
+		}
+		if config.KubeConfig != "test" {
+			t.Fatalf("Unexpected kubeconfig value: %v", config.KubeConfig)
+		}
+		if config.ListOutput != "yaml" {
+			t.Fatalf("Unexpected list_output value: %v", config.ListOutput)
+		}
+		if !config.ReadOnly {
+			t.Fatalf("Unexpected read-only mode: %v", config.ReadOnly)
+		}
+		if config.DisableDestructive {
+			t.Fatalf("Unexpected disable destructive: %v", config.DisableDestructive)
+		}
 	})
 }
 
diff --git a/pkg/kubernetes-mcp-server/cmd/root_test.go b/pkg/kubernetes-mcp-server/cmd/root_test.go
index 85b1861..13bd5c7 100644
--- a/pkg/kubernetes-mcp-server/cmd/root_test.go
+++ b/pkg/kubernetes-mcp-server/cmd/root_test.go
@@ -79,6 +79,54 @@ func TestConfig(t *testing.T) {
 			t.Fatalf("Expected error to be %s, got %s", expected, err.Error())
 		}
 	})
+	t.Run("set with valid --config", func(t *testing.T) {
+		ioStreams, out := testStream()
+		rootCmd := NewMCPServer(ioStreams)
+		_, file, _, _ := runtime.Caller(0)
+		validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
+		rootCmd.SetArgs([]string{"--version", "--config", validConfigPath})
+		_ = rootCmd.Execute()
+		expectedConfig := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
+		if m, err := regexp.MatchString(expectedConfig, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedConfig, out.String(), err)
+		}
+		expectedListOutput := `(?m)\" - ListOutput\: yaml"`
+		if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
+		}
+		expectedReadOnly := `(?m)\" - Read-only mode: true"`
+		if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
+		}
+		expectedDisableDestruction := `(?m)\" - Disable destructive tools: true"`
+		if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
+		}
+	})
+	t.Run("set with valid --config, flags override", func(t *testing.T) {
+		ioStreams, out := testStream()
+		rootCmd := NewMCPServer(ioStreams)
+		_, file, _, _ := runtime.Caller(0)
+		validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
+		rootCmd.SetArgs([]string{"--version", "--list-output=table", "--disable-destructive=false", "--read-only=false", "--config", validConfigPath})
+		_ = rootCmd.Execute()
+		expected := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
+		if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expected, out.String(), err)
+		}
+		expectedListOutput := `(?m)\" - ListOutput\: table"`
+		if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
+		}
+		expectedReadOnly := `(?m)\" - Read-only mode: false"`
+		if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
+		}
+		expectedDisableDestruction := `(?m)\" - Disable destructive tools: false"`
+		if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
+			t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
+		}
+	})
 }
 
 func TestProfile(t *testing.T) {
diff --git a/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml b/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml
new file mode 100644
index 0000000..bb8da16
--- /dev/null
+++ b/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml
@@ -0,0 +1,15 @@
+log_level = 1
+sse_port = 9999
+kubeconfig = "test"
+list_output = "yaml"
+read_only = true
+disable_destructive = true
+
+[[denied_resources]]
+group = "apps"
+version = "v1"
+kind = "Deployment"
+
+[[denied_resources]]
+group = "rbac.authorization.k8s.io"
+version = "v1"
\ No newline at end of file
diff --git a/pkg/mcp/events_test.go b/pkg/mcp/events_test.go
index 1d3c2f0..cc50316 100644
--- a/pkg/mcp/events_test.go
+++ b/pkg/mcp/events_test.go
@@ -108,7 +108,7 @@ func TestEventsListDenied(t *testing.T) {
 		t.Run("events_list describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list events in all namespaces: resource not allowed: /v1, Kind=Event"
 			if eventList.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
diff --git a/pkg/mcp/helm_test.go b/pkg/mcp/helm_test.go
index 6eda026..4f0c4fe 100644
--- a/pkg/mcp/helm_test.go
+++ b/pkg/mcp/helm_test.go
@@ -59,7 +59,6 @@ func TestHelmInstall(t *testing.T) {
 }
 
 func TestHelmInstallDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: helm_install is not checking for denied resources
 	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Secret"}}}
 	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
 		c.withEnvTest()
@@ -74,9 +73,10 @@ func TestHelmInstallDenied(t *testing.T) {
 			}
 		})
 		t.Run("helm_install describes denial", func(t *testing.T) {
-			expectedMessage := "failed to install helm chart: resource not allowed: /v1, Kind=Secret"
-			if helmInstall.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, helmInstall.Content[0].(mcp.TextContent).Text)
+			toolOutput := helmInstall.Content[0].(mcp.TextContent).Text
+			expectedMessage := ": resource not allowed: /v1, Kind=Secret"
+			if !strings.HasPrefix(toolOutput, "failed to install helm chart") || !strings.HasSuffix(toolOutput, expectedMessage) {
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, helmInstall.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -225,7 +225,33 @@ func TestHelmUninstall(t *testing.T) {
 }
 
 func TestHelmUninstallDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: helm_uninstall is not checking for denied resources
+	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Secret"}}}
+	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
+		c.withEnvTest()
+		kc := c.newKubernetesClient()
+		clearHelmReleases(c.ctx, kc)
+		_, _ = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:   "sh.helm.release.v1.existent-release-to-uninstall.v0",
+				Labels: map[string]string{"owner": "helm", "name": "existent-release-to-uninstall"},
+			},
+			Data: map[string][]byte{
+				"release": []byte(base64.StdEncoding.EncodeToString([]byte("{" +
+					"\"name\":\"existent-release-to-uninstall\"," +
+					"\"info\":{\"status\":\"deployed\"}," +
+					"\"manifest\":\"apiVersion: v1\\nkind: Secret\\nmetadata:\\n  name: secret-to-deny\\n  namespace: default\\n\"" +
+					"}"))),
+			},
+		}, metav1.CreateOptions{})
+		helmUninstall, _ := c.callTool("helm_uninstall", map[string]interface{}{
+			"name": "existent-release-to-uninstall",
+		})
+		t.Run("helm_uninstall has error", func(t *testing.T) {
+			if !helmUninstall.IsError {
+				t.Fatalf("call tool should fail")
+			}
+		})
+	})
 }
 
 func clearHelmReleases(ctx context.Context, kc *kubernetes.Clientset) {
diff --git a/pkg/mcp/namespaces_test.go b/pkg/mcp/namespaces_test.go
index 05280dc..b8b884d 100644
--- a/pkg/mcp/namespaces_test.go
+++ b/pkg/mcp/namespaces_test.go
@@ -62,7 +62,7 @@ func TestNamespacesListDenied(t *testing.T) {
 		t.Run("namespaces_list describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list namespaces: resource not allowed: /v1, Kind=Namespace"
 			if namespacesList.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, namespacesList.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, namespacesList.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -167,7 +167,7 @@ func TestProjectsListInOpenShiftDenied(t *testing.T) {
 		t.Run("projects_list describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list projects: resource not allowed: project.openshift.io/v1, Kind=Project"
 			if projectsList.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
diff --git a/pkg/mcp/pods_exec_test.go b/pkg/mcp/pods_exec_test.go
index b869ddc..3303f4f 100644
--- a/pkg/mcp/pods_exec_test.go
+++ b/pkg/mcp/pods_exec_test.go
@@ -2,6 +2,7 @@ package mcp
 
 import (
 	"bytes"
+	"github.com/manusa/kubernetes-mcp-server/pkg/config"
 	"github.com/mark3labs/mcp-go/mcp"
 	"io"
 	v1 "k8s.io/api/core/v1"
@@ -101,5 +102,25 @@ func TestPodsExec(t *testing.T) {
 }
 
 func TestPodsExecDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: exec is not checking for denied resources
+	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
+	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
+		c.withEnvTest()
+		podsRun, _ := c.callTool("pods_exec", map[string]interface{}{
+			"namespace": "default",
+			"name":      "pod-to-exec",
+			"command":   []interface{}{"ls", "-l"},
+			"container": "a-specific-container",
+		})
+		t.Run("pods_exec has error", func(t *testing.T) {
+			if !podsRun.IsError {
+				t.Fatalf("call tool should fail")
+			}
+		})
+		t.Run("pods_exec describes denial", func(t *testing.T) {
+			expectedMessage := "failed to exec in pod pod-to-exec in namespace default: resource not allowed: /v1, Kind=Pod"
+			if podsRun.Content[0].(mcp.TextContent).Text != expectedMessage {
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
+			}
+		})
+	})
 }
diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go
index d3a2273..783f7a5 100644
--- a/pkg/mcp/pods_test.go
+++ b/pkg/mcp/pods_test.go
@@ -190,7 +190,7 @@ func TestPodsListDenied(t *testing.T) {
 		t.Run("pods_list describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list pods in all namespaces: resource not allowed: /v1, Kind=Pod"
 			if podsList.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsList.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsList.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		podsListInNamespace, _ := c.callTool("pods_list_in_namespace", map[string]interface{}{"namespace": "ns-1"})
@@ -202,7 +202,7 @@ func TestPodsListDenied(t *testing.T) {
 		t.Run("pods_list_in_namespace describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list pods in namespace ns-1: resource not allowed: /v1, Kind=Pod"
 			if podsListInNamespace.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsListInNamespace.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsListInNamespace.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -425,7 +425,7 @@ func TestPodsGetDenied(t *testing.T) {
 		t.Run("pods_get describes denial", func(t *testing.T) {
 			expectedMessage := "failed to get pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
 			if podsGet.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsGet.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsGet.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -563,7 +563,6 @@ func TestPodsDelete(t *testing.T) {
 }
 
 func TestPodsDeleteDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: delete is not checking for denied resources
 	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
 	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
 		c.withEnvTest()
@@ -576,7 +575,7 @@ func TestPodsDeleteDenied(t *testing.T) {
 		t.Run("pods_delete describes denial", func(t *testing.T) {
 			expectedMessage := "failed to delete pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
 			if podsDelete.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsDelete.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsDelete.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -723,7 +722,6 @@ func TestPodsLog(t *testing.T) {
 }
 
 func TestPodsLogDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: log is not checking for denied resources
 	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
 	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
 		c.withEnvTest()
@@ -734,9 +732,9 @@ func TestPodsLogDenied(t *testing.T) {
 			}
 		})
 		t.Run("pods_log describes denial", func(t *testing.T) {
-			expectedMessage := "failed to log pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
+			expectedMessage := "failed to get pod a-pod-in-default log in namespace : resource not allowed: /v1, Kind=Pod"
 			if podsLog.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsLog.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsLog.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
@@ -905,7 +903,7 @@ func TestPodsRunDenied(t *testing.T) {
 		t.Run("pods_run describes denial", func(t *testing.T) {
 			expectedMessage := "failed to run pod  in namespace : resource not allowed: /v1, Kind=Pod"
 			if podsRun.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
 			}
 		})
 	})
diff --git a/pkg/mcp/pods_top_test.go b/pkg/mcp/pods_top_test.go
index 8ef6f6f..f45add0 100644
--- a/pkg/mcp/pods_top_test.go
+++ b/pkg/mcp/pods_top_test.go
@@ -1,10 +1,13 @@
 package mcp
 
 import (
-	"github.com/mark3labs/mcp-go/mcp"
 	"net/http"
 	"regexp"
 	"testing"
+
+	"github.com/mark3labs/mcp-go/mcp"
+
+	"github.com/manusa/kubernetes-mcp-server/pkg/config"
 )
 
 func TestPodsTopMetricsUnavailable(t *testing.T) {
@@ -206,5 +209,40 @@ func TestPodsTopMetricsAvailable(t *testing.T) {
 }
 
 func TestPodsTopDenied(t *testing.T) {
-	t.Skip("To be implemented") // TODO: top is not checking for denied resources
+	deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Group: "metrics.k8s.io", Version: "v1beta1"}}}
+	testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
+		mockServer := NewMockServer()
+		defer mockServer.Close()
+		c.withKubeConfig(mockServer.config)
+		mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
+			if req.URL.Path == "/api" {
+				_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["metrics.k8s.io/v1beta1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
+				return
+			}
+			// Request Performed by DiscoveryClient to Kube API (Get API Groups)
+			if req.URL.Path == "/apis" {
+				_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
+				return
+			}
+			// Request Performed by DiscoveryClient to Kube API (Get API Resources)
+			if req.URL.Path == "/apis/metrics.k8s.io/v1beta1" {
+				_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"pods","singularName":"","namespaced":true,"kind":"PodMetrics","verbs":["get","list"]}]}`))
+				return
+			}
+		}))
+		podsTop, _ := c.callTool("pods_top", map[string]interface{}{})
+		t.Run("pods_run has error", func(t *testing.T) {
+			if !podsTop.IsError {
+				t.Fatalf("call tool should fail")
+			}
+		})
+		t.Run("pods_run describes denial", func(t *testing.T) {
+			expectedMessage := "failed to get pods top: resource not allowed: metrics.k8s.io/v1beta1, Kind=PodMetrics"
+			if podsTop.Content[0].(mcp.TextContent).Text != expectedMessage {
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsTop.Content[0].(mcp.TextContent).Text)
+			}
+		})
+	})
 }
diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go
index 7d17676..b62dc81 100644
--- a/pkg/mcp/resources_test.go
+++ b/pkg/mcp/resources_test.go
@@ -169,7 +169,7 @@ func TestResourcesListDenied(t *testing.T) {
 		t.Run("resources_list (denied by kind) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list resources: resource not allowed: /v1, Kind=Secret"
 			if deniedByKind.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		deniedByGroup, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role"})
@@ -181,7 +181,7 @@ func TestResourcesListDenied(t *testing.T) {
 		t.Run("resources_list (denied by group) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to list resources: resource not allowed: rbac.authorization.k8s.io/v1, Kind=Role"
 			if deniedByGroup.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		allowedResource, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
@@ -381,7 +381,7 @@ func TestResourcesGetDenied(t *testing.T) {
 		t.Run("resources_get (denied by kind) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to get resource: resource not allowed: /v1, Kind=Secret"
 			if deniedByKind.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		deniedByGroup, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "namespace": "default", "name": "denied-role"})
@@ -393,7 +393,7 @@ func TestResourcesGetDenied(t *testing.T) {
 		t.Run("resources_get (denied by group) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to get resource: resource not allowed: rbac.authorization.k8s.io/v1, Kind=Role"
 			if deniedByGroup.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		allowedResource, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace", "name": "default"})
@@ -601,7 +601,7 @@ func TestResourcesCreateOrUpdateDenied(t *testing.T) {
 		t.Run("resources_create_or_update (denied by kind) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to create or update resources: resource not allowed: /v1, Kind=Secret"
 			if deniedByKind.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		roleYaml := "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: a-denied-role\n  namespace: default\n"
@@ -614,7 +614,7 @@ func TestResourcesCreateOrUpdateDenied(t *testing.T) {
 		t.Run("resources_create_or_update (denied by group) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to create or update resources: resource not allowed: rbac.authorization.k8s.io/v1, Kind=Role"
 			if deniedByGroup.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		configMapYaml := "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: a-cm-created-or-updated\n  namespace: default\n"
@@ -766,7 +766,7 @@ func TestResourcesDeleteDenied(t *testing.T) {
 		t.Run("resources_delete (denied by kind) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to delete resource: resource not allowed: /v1, Kind=Secret"
 			if deniedByKind.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		deniedByGroup, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "namespace": "default", "name": "denied-role"})
@@ -778,7 +778,7 @@ func TestResourcesDeleteDenied(t *testing.T) {
 		t.Run("resources_delete (denied by group) describes denial", func(t *testing.T) {
 			expectedMessage := "failed to delete resource: resource not allowed: rbac.authorization.k8s.io/v1, Kind=Role"
 			if deniedByGroup.Content[0].(mcp.TextContent).Text != expectedMessage {
-				t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
+				t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, deniedByKind.Content[0].(mcp.TextContent).Text)
 			}
 		})
 		allowedResource, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "allowed-configmap-to-delete"})