Repository: containers/kubernetes-mcp-server

Issue:
Refactor tests to merge multiple json files
To make it easier to extend toolsets in forks, like the openshift-mcp-server, we should refactor the mcp/toolset tests to:
1. Have tools from each toolset defined in it's own json file
2. Merge those tools into the final expected object based on which toolsets should be enabled in the scenario
3. Make assertions about equality

Hints from issue comments:
@Cali0707: cc @manusa @matzew @manusa: The tests already have each toolset defined in its own toolset json file (1).

I'm assuming the related problem is about the `toolsets-full-tools.json` which has the combined toolsets.

Note that this IS NOT including **all** toolsets but the ones defined as **enabled by default**:

https://github.com/containers/kubernetes-mcp-server/blob/86628bb1bf6615b2c5052602c0f9f13a41b93305/pkg/config/config.go#L76

I implemented these tests while creating the decoupled toolsets feature to ensure that the tool definitions remained constant.
I see how the full-tools toolset test is a pain for maintainers (the others too unless you know how to update the files).

We can either remove the full-tools test, add a flag so that the test is skipped downstream, find an alternative.
I personally like having those because it gives an extra degree of confidence.


@manusa: OK; after checking the rest of the activity I can see better the issue now.

IMO downstream we should not mutate the existing toolsets, i.e. Don't add stuff to core, helm, config, etc.

If there's a need to extend one of the original toolsets, then just create a new one and add the extended tools. For example `openshift-core`.

Or created a granular one `openshift-debug`. With the isolated group of tools to add ([then maybe add it to the default list of toolsets](https://github.com/containers/kubernetes-mcp-server/pull/396)).

The idea of toolsets was to make them **completely decoupled** from the upstream so that downstream you could add separate directories and each toolset self-registred.

By following these principles, the only points of divergence are introduced in:
- [pkg/mcp/modules.go](https://github.com/containers/kubernetes-mcp-server/blob/48cf204a897168cb7db9f69b044d68525d891f8d/pkg/mcp/modules.go).
  We can further mitigate this by creating an empty `modules_downstream.go`
- [pkg/mcp/modules.go](https://github.com/containers/kubernetes-mcp-server/blob/86628bb1bf6615b2c5052602c0f9f13a41b93305/pkg/config/config.go#L73-L78)
  Could also be mitigated by creating a default config provider@manusa: The second commit in https://github.com/containers/kubernetes-mcp-server/pull/396 should take care of the problem

Solution patch:
diff --git a/AGENTS.md b/AGENTS.md
index 854cfe5..485ac1a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,12 +11,15 @@ This MCP server enables AI assistants (like Claude, Gemini, Cursor, and others)
 - Go package layout follows the standard Go conventions:
   - `cmd/kubernetes-mcp-server/` – main application entry point using Cobra CLI framework.
   - `pkg/` – libraries grouped by domain.
+    - `api/` - API-related functionality, tool definitions, and toolset interfaces.
     - `config/` – configuration management.
     - `helm/` - Helm chart operations integration.
     - `http/` - HTTP server and authorization middleware.
     - `kubernetes/` - Kubernetes client management, authentication, and access control.
     - `mcp/` - Model Context Protocol (MCP) server implementation with tool registration and STDIO/HTTP support.
     - `output/` - output formatting and rendering.
+    - `toolsets/` - Toolset registration and management for MCP tools.
+    - `version/` - Version information management.
 - `.github/` – GitHub-related configuration (Actions workflows, issue templates...).
 - `docs/` – documentation files.
 - `npm/` – Node packages that wraps the compiled binaries for distribution through npmjs.com.
@@ -30,6 +33,21 @@ Implement new functionality in the Go sources under `cmd/` and `pkg/`.
 The JavaScript (`npm/`) and Python (`python/`) directories only wrap the compiled binary for distribution (npm and PyPI).
 Most changes will not require touching them unless the version or packaging needs to be updated.
 
+### Adding new MCP tools
+
+The project uses a toolset-based architecture for organizing MCP tools:
+
+- **Tool definitions** are created in `pkg/api/` using the `ServerTool` struct.
+- **Toolsets** group related tools together (e.g., config tools, core Kubernetes tools, Helm tools).
+- **Registration** happens in `pkg/toolsets/` where toolsets are registered at initialization.
+- Each toolset lives in its own subdirectory under `pkg/toolsets/` (e.g., `pkg/toolsets/config/`, `pkg/toolsets/core/`, `pkg/toolsets/helm/`).
+
+When adding a new tool:
+1. Define the tool handler function that implements the tool's logic.
+2. Create a `ServerTool` struct with the tool definition and handler.
+3. Add the tool to an appropriate toolset (or create a new toolset if needed).
+4. Register the toolset in `pkg/toolsets/` if it's a new toolset.
+
 ## Building
 
 Use the provided Makefile targets:
@@ -105,6 +123,45 @@ make lint
 
 The `lint` target downloads the specified `golangci-lint` version if it is not already present under `_output/tools/bin/`.
 
+## Additional Makefile targets
+
+Beyond the basic build, test, and lint targets, the Makefile provides additional utilities:
+
+**Local Development:**
+```bash
+# Setup a complete local development environment with Kind cluster
+make local-env-setup
+
+# Tear down the local Kind cluster
+make local-env-teardown
+
+# Show Keycloak status and connection info (for OIDC testing)
+make keycloak-status
+
+# Tail Keycloak logs
+make keycloak-logs
+
+# Install required development tools (like Kind) to ./_output/bin/
+make tools
+```
+
+**Distribution and Publishing:**
+```bash
+# Copy compiled binaries to each npm package
+make npm-copy-binaries
+
+# Publish the npm packages
+make npm-publish
+
+# Publish the Python packages
+make python-publish
+
+# Update README.md with the latest toolsets
+make update-readme-tools
+```
+
+Run `make help` to see all available targets with descriptions.
+
 ## Dependencies
 
 When introducing new modules run `make tidy` so that `go.mod` and `go.sum` remain tidy.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 0000000..47dc3e3
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/build/keycloak.mk b/build/keycloak.mk
index 2a7a929..d541c8b 100644
--- a/build/keycloak.mk
+++ b/build/keycloak.mk
@@ -111,7 +111,7 @@ keycloak-setup-realm:
 	REALM_RESPONSE=$$(curl -sk -w "%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"realm":"openshift","enabled":true}'); \
+		-d @dev/config/keycloak/realm/realm-create.json); \
 	REALM_CODE=$$(echo "$$REALM_RESPONSE" | tail -c 4); \
 	if [ "$$REALM_CODE" = "201" ] || [ "$$REALM_CODE" = "409" ]; then \
 		if [ "$$REALM_CODE" = "201" ]; then echo "✅ OpenShift realm created"; \
@@ -125,7 +125,7 @@ keycloak-setup-realm:
 	EVENT_CONFIG_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X PUT "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"realm":"openshift","enabled":true,"eventsEnabled":true,"eventsListeners":["jboss-logging"],"adminEventsEnabled":true,"adminEventsDetailsEnabled":true}'); \
+		-d @dev/config/keycloak/realm/realm-events-config.json); \
 	EVENT_CONFIG_CODE=$$(echo "$$EVENT_CONFIG_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$EVENT_CONFIG_CODE" = "204" ]; then \
 		echo "✅ User and admin event logging enabled"; \
@@ -137,7 +137,7 @@ keycloak-setup-realm:
 	SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"mcp:openshift","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
+		-d @dev/config/keycloak/client-scopes/mcp-openshift.json); \
 	SCOPE_CODE=$$(echo "$$SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$SCOPE_CODE" = "201" ] || [ "$$SCOPE_CODE" = "409" ]; then \
 		if [ "$$SCOPE_CODE" = "201" ]; then echo "✅ mcp:openshift client scope created"; \
@@ -159,7 +159,7 @@ keycloak-setup-realm:
 	MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$SCOPE_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"openshift-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"openshift","id.token.claim":"true","access.token.claim":"true"}}'); \
+		-d @dev/config/keycloak/mappers/openshift-audience.json); \
 	MAPPER_CODE=$$(echo "$$MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MAPPER_CODE" = "201" ] || [ "$$MAPPER_CODE" = "409" ]; then \
 		if [ "$$MAPPER_CODE" = "201" ]; then echo "✅ Audience mapper added"; \
@@ -173,7 +173,7 @@ keycloak-setup-realm:
 	GROUPS_SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"groups","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
+		-d @dev/config/keycloak/client-scopes/groups.json); \
 	GROUPS_SCOPE_CODE=$$(echo "$$GROUPS_SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$GROUPS_SCOPE_CODE" = "201" ] || [ "$$GROUPS_SCOPE_CODE" = "409" ]; then \
 		if [ "$$GROUPS_SCOPE_CODE" = "201" ]; then echo "✅ groups client scope created"; \
@@ -195,7 +195,7 @@ keycloak-setup-realm:
 	GROUPS_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$GROUPS_SCOPE_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"groups","protocol":"openid-connect","protocolMapper":"oidc-group-membership-mapper","config":{"claim.name":"groups","full.path":"false","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true"}}'); \
+		-d @dev/config/keycloak/mappers/groups-membership.json); \
 	GROUPS_MAPPER_CODE=$$(echo "$$GROUPS_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$GROUPS_MAPPER_CODE" = "201" ] || [ "$$GROUPS_MAPPER_CODE" = "409" ]; then \
 		if [ "$$GROUPS_MAPPER_CODE" = "201" ]; then echo "✅ Group membership mapper added"; \
@@ -209,7 +209,7 @@ keycloak-setup-realm:
 	MCP_SERVER_SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"mcp-server","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
+		-d @dev/config/keycloak/client-scopes/mcp-server.json); \
 	MCP_SERVER_SCOPE_CODE=$$(echo "$$MCP_SERVER_SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_SERVER_SCOPE_CODE" = "201" ] || [ "$$MCP_SERVER_SCOPE_CODE" = "409" ]; then \
 		if [ "$$MCP_SERVER_SCOPE_CODE" = "201" ]; then echo "✅ mcp-server client scope created"; \
@@ -231,7 +231,7 @@ keycloak-setup-realm:
 	MCP_SERVER_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$MCP_SERVER_SCOPE_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"mcp-server-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"mcp-server","id.token.claim":"true","access.token.claim":"true"}}'); \
+		-d @dev/config/keycloak/mappers/mcp-server-audience.json); \
 	MCP_SERVER_MAPPER_CODE=$$(echo "$$MCP_SERVER_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_SERVER_MAPPER_CODE" = "201" ] || [ "$$MCP_SERVER_MAPPER_CODE" = "409" ]; then \
 		if [ "$$MCP_SERVER_MAPPER_CODE" = "201" ]; then echo "✅ mcp-server audience mapper added"; \
@@ -245,7 +245,7 @@ keycloak-setup-realm:
 	OPENSHIFT_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"clientId":"openshift","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups"],"optionalClientScopes":[]}'); \
+		-d @dev/config/keycloak/clients/openshift.json); \
 	OPENSHIFT_CLIENT_CODE=$$(echo "$$OPENSHIFT_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$OPENSHIFT_CLIENT_CODE" = "201" ] || [ "$$OPENSHIFT_CLIENT_CODE" = "409" ]; then \
 		if [ "$$OPENSHIFT_CLIENT_CODE" = "201" ]; then echo "✅ openshift client created"; \
@@ -263,7 +263,7 @@ keycloak-setup-realm:
 	OPENSHIFT_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$OPENSHIFT_CLIENT_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{	"name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
+		-d @dev/config/keycloak/mappers/username.json); \
 	OPENSHIFT_USERNAME_MAPPER_CODE=$$(echo "$$OPENSHIFT_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "201" ] || [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "409" ]; then \
 		if [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to openshift client"; \
@@ -277,7 +277,7 @@ keycloak-setup-realm:
 	MCP_PUBLIC_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"clientId":"mcp-client","enabled":true,"publicClient":true,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":false,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email"],"optionalClientScopes":["mcp-server"]}'); \
+		-d @dev/config/keycloak/clients/mcp-client.json); \
 	MCP_PUBLIC_CLIENT_CODE=$$(echo "$$MCP_PUBLIC_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_PUBLIC_CLIENT_CODE" = "201" ] || [ "$$MCP_PUBLIC_CLIENT_CODE" = "409" ]; then \
 		if [ "$$MCP_PUBLIC_CLIENT_CODE" = "201" ]; then echo "✅ mcp-client public client created"; \
@@ -295,7 +295,7 @@ keycloak-setup-realm:
 	MCP_PUBLIC_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_PUBLIC_CLIENT_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
+		-d @dev/config/keycloak/mappers/username.json); \
 	MCP_PUBLIC_USERNAME_MAPPER_CODE=$$(echo "$$MCP_PUBLIC_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "201" ] || [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "409" ]; then \
 		if [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to mcp-client"; \
@@ -309,7 +309,7 @@ keycloak-setup-realm:
 	MCP_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"clientId":"mcp-server","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups","mcp-server"],"optionalClientScopes":["mcp:openshift"],"attributes":{"oauth2.device.authorization.grant.enabled":"false","oidc.ciba.grant.enabled":"false","backchannel.logout.session.required":"true","backchannel.logout.revoke.offline.tokens":"false"}}'); \
+		-d @dev/config/keycloak/clients/mcp-server.json); \
 	MCP_CLIENT_CODE=$$(echo "$$MCP_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_CLIENT_CODE" = "201" ] || [ "$$MCP_CLIENT_CODE" = "409" ]; then \
 		if [ "$$MCP_CLIENT_CODE" = "201" ]; then echo "✅ mcp-server client created"; \
@@ -331,7 +331,7 @@ keycloak-setup-realm:
 	UPDATE_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X PUT "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_CLIENT_ID" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"clientId":"mcp-server","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups","mcp-server"],"optionalClientScopes":["mcp:openshift"],"attributes":{"oauth2.device.authorization.grant.enabled":"false","oidc.ciba.grant.enabled":"false","backchannel.logout.session.required":"true","backchannel.logout.revoke.offline.tokens":"false","standard.token.exchange.enabled":"true"}}'); \
+		-d @dev/config/keycloak/clients/mcp-server-update.json); \
 	UPDATE_CLIENT_CODE=$$(echo "$$UPDATE_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$UPDATE_CLIENT_CODE" = "204" ]; then \
 		echo "✅ Standard token exchange enabled for mcp-server client"; \
@@ -354,7 +354,7 @@ keycloak-setup-realm:
 	MCP_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_CLIENT_ID/protocol-mappers/models" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
+		-d @dev/config/keycloak/mappers/username.json); \
 	MCP_USERNAME_MAPPER_CODE=$$(echo "$$MCP_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
 	if [ "$$MCP_USERNAME_MAPPER_CODE" = "201" ] || [ "$$MCP_USERNAME_MAPPER_CODE" = "409" ]; then \
 		if [ "$$MCP_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to mcp-server client"; \
@@ -368,7 +368,7 @@ keycloak-setup-realm:
 	USER_RESPONSE=$$(curl -sk -w "%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/users" \
 		-H "Authorization: Bearer $$TOKEN" \
 		-H "Content-Type: application/json" \
-		-d '{"username":"mcp","email":"mcp@example.com","firstName":"MCP","lastName":"User","enabled":true,"emailVerified":true,"credentials":[{"type":"password","value":"mcp","temporary":false}]}'); \
+		-d @dev/config/keycloak/users/mcp.json); \
 	USER_CODE=$$(echo "$$USER_RESPONSE" | tail -c 4); \
 	if [ "$$USER_CODE" = "201" ] || [ "$$USER_CODE" = "409" ]; then \
 		if [ "$$USER_CODE" = "201" ]; then echo "✅ mcp user created"; \
diff --git a/dev/config/keycloak/client-scopes/groups.json b/dev/config/keycloak/client-scopes/groups.json
new file mode 100644
index 0000000..4eb20b7
--- /dev/null
+++ b/dev/config/keycloak/client-scopes/groups.json
@@ -0,0 +1,8 @@
+{
+  "name": "groups",
+  "protocol": "openid-connect",
+  "attributes": {
+    "display.on.consent.screen": "false",
+    "include.in.token.scope": "true"
+  }
+}
diff --git a/dev/config/keycloak/client-scopes/mcp-openshift.json b/dev/config/keycloak/client-scopes/mcp-openshift.json
new file mode 100644
index 0000000..39f55e7
--- /dev/null
+++ b/dev/config/keycloak/client-scopes/mcp-openshift.json
@@ -0,0 +1,8 @@
+{
+  "name": "mcp:openshift",
+  "protocol": "openid-connect",
+  "attributes": {
+    "display.on.consent.screen": "false",
+    "include.in.token.scope": "true"
+  }
+}
diff --git a/dev/config/keycloak/client-scopes/mcp-server.json b/dev/config/keycloak/client-scopes/mcp-server.json
new file mode 100644
index 0000000..5ac0440
--- /dev/null
+++ b/dev/config/keycloak/client-scopes/mcp-server.json
@@ -0,0 +1,8 @@
+{
+  "name": "mcp-server",
+  "protocol": "openid-connect",
+  "attributes": {
+    "display.on.consent.screen": "false",
+    "include.in.token.scope": "true"
+  }
+}
diff --git a/dev/config/keycloak/clients/mcp-client.json b/dev/config/keycloak/clients/mcp-client.json
new file mode 100644
index 0000000..7e353e8
--- /dev/null
+++ b/dev/config/keycloak/clients/mcp-client.json
@@ -0,0 +1,12 @@
+{
+  "clientId": "mcp-client",
+  "enabled": true,
+  "publicClient": true,
+  "standardFlowEnabled": true,
+  "directAccessGrantsEnabled": true,
+  "serviceAccountsEnabled": false,
+  "authorizationServicesEnabled": false,
+  "redirectUris": ["*"],
+  "defaultClientScopes": ["profile", "email"],
+  "optionalClientScopes": ["mcp-server"]
+}
diff --git a/dev/config/keycloak/clients/mcp-server-update.json b/dev/config/keycloak/clients/mcp-server-update.json
new file mode 100644
index 0000000..8af21f1
--- /dev/null
+++ b/dev/config/keycloak/clients/mcp-server-update.json
@@ -0,0 +1,19 @@
+{
+  "clientId": "mcp-server",
+  "enabled": true,
+  "publicClient": false,
+  "standardFlowEnabled": true,
+  "directAccessGrantsEnabled": true,
+  "serviceAccountsEnabled": true,
+  "authorizationServicesEnabled": false,
+  "redirectUris": ["*"],
+  "defaultClientScopes": ["profile", "email", "groups", "mcp-server"],
+  "optionalClientScopes": ["mcp:openshift"],
+  "attributes": {
+    "oauth2.device.authorization.grant.enabled": "false",
+    "oidc.ciba.grant.enabled": "false",
+    "backchannel.logout.session.required": "true",
+    "backchannel.logout.revoke.offline.tokens": "false",
+    "standard.token.exchange.enabled": "true"
+  }
+}
diff --git a/dev/config/keycloak/clients/mcp-server.json b/dev/config/keycloak/clients/mcp-server.json
new file mode 100644
index 0000000..fcabacb
--- /dev/null
+++ b/dev/config/keycloak/clients/mcp-server.json
@@ -0,0 +1,18 @@
+{
+  "clientId": "mcp-server",
+  "enabled": true,
+  "publicClient": false,
+  "standardFlowEnabled": true,
+  "directAccessGrantsEnabled": true,
+  "serviceAccountsEnabled": true,
+  "authorizationServicesEnabled": false,
+  "redirectUris": ["*"],
+  "defaultClientScopes": ["profile", "email", "groups", "mcp-server"],
+  "optionalClientScopes": ["mcp:openshift"],
+  "attributes": {
+    "oauth2.device.authorization.grant.enabled": "false",
+    "oidc.ciba.grant.enabled": "false",
+    "backchannel.logout.session.required": "true",
+    "backchannel.logout.revoke.offline.tokens": "false"
+  }
+}
diff --git a/dev/config/keycloak/clients/openshift.json b/dev/config/keycloak/clients/openshift.json
new file mode 100644
index 0000000..0c60317
--- /dev/null
+++ b/dev/config/keycloak/clients/openshift.json
@@ -0,0 +1,12 @@
+{
+  "clientId": "openshift",
+  "enabled": true,
+  "publicClient": false,
+  "standardFlowEnabled": true,
+  "directAccessGrantsEnabled": true,
+  "serviceAccountsEnabled": true,
+  "authorizationServicesEnabled": false,
+  "redirectUris": ["*"],
+  "defaultClientScopes": ["profile", "email", "groups"],
+  "optionalClientScopes": []
+}
diff --git a/dev/config/keycloak/mappers/groups-membership.json b/dev/config/keycloak/mappers/groups-membership.json
new file mode 100644
index 0000000..266a66e
--- /dev/null
+++ b/dev/config/keycloak/mappers/groups-membership.json
@@ -0,0 +1,12 @@
+{
+  "name": "groups",
+  "protocol": "openid-connect",
+  "protocolMapper": "oidc-group-membership-mapper",
+  "config": {
+    "claim.name": "groups",
+    "full.path": "false",
+    "id.token.claim": "true",
+    "access.token.claim": "true",
+    "userinfo.token.claim": "true"
+  }
+}
diff --git a/dev/config/keycloak/mappers/mcp-server-audience.json b/dev/config/keycloak/mappers/mcp-server-audience.json
new file mode 100644
index 0000000..37b7e96
--- /dev/null
+++ b/dev/config/keycloak/mappers/mcp-server-audience.json
@@ -0,0 +1,10 @@
+{
+  "name": "mcp-server-audience",
+  "protocol": "openid-connect",
+  "protocolMapper": "oidc-audience-mapper",
+  "config": {
+    "included.client.audience": "mcp-server",
+    "id.token.claim": "true",
+    "access.token.claim": "true"
+  }
+}
diff --git a/dev/config/keycloak/mappers/openshift-audience.json b/dev/config/keycloak/mappers/openshift-audience.json
new file mode 100644
index 0000000..74b84b7
--- /dev/null
+++ b/dev/config/keycloak/mappers/openshift-audience.json
@@ -0,0 +1,10 @@
+{
+  "name": "openshift-audience",
+  "protocol": "openid-connect",
+  "protocolMapper": "oidc-audience-mapper",
+  "config": {
+    "included.client.audience": "openshift",
+    "id.token.claim": "true",
+    "access.token.claim": "true"
+  }
+}
diff --git a/dev/config/keycloak/mappers/username.json b/dev/config/keycloak/mappers/username.json
new file mode 100644
index 0000000..d76ccfa
--- /dev/null
+++ b/dev/config/keycloak/mappers/username.json
@@ -0,0 +1,13 @@
+{
+  "name": "username",
+  "protocol": "openid-connect",
+  "protocolMapper": "oidc-usermodel-property-mapper",
+  "config": {
+    "userinfo.token.claim": "true",
+    "user.attribute": "username",
+    "id.token.claim": "true",
+    "access.token.claim": "true",
+    "claim.name": "preferred_username",
+    "jsonType.label": "String"
+  }
+}
diff --git a/dev/config/keycloak/realm/realm-create.json b/dev/config/keycloak/realm/realm-create.json
new file mode 100644
index 0000000..d651e7d
--- /dev/null
+++ b/dev/config/keycloak/realm/realm-create.json
@@ -0,0 +1,4 @@
+{
+  "realm": "openshift",
+  "enabled": true
+}
diff --git a/dev/config/keycloak/realm/realm-events-config.json b/dev/config/keycloak/realm/realm-events-config.json
new file mode 100644
index 0000000..72b07a5
--- /dev/null
+++ b/dev/config/keycloak/realm/realm-events-config.json
@@ -0,0 +1,8 @@
+{
+  "realm": "openshift",
+  "enabled": true,
+  "eventsEnabled": true,
+  "eventsListeners": ["jboss-logging"],
+  "adminEventsEnabled": true,
+  "adminEventsDetailsEnabled": true
+}
diff --git a/dev/config/keycloak/users/mcp.json b/dev/config/keycloak/users/mcp.json
new file mode 100644
index 0000000..b84bc3f
--- /dev/null
+++ b/dev/config/keycloak/users/mcp.json
@@ -0,0 +1,15 @@
+{
+  "username": "mcp",
+  "email": "mcp@example.com",
+  "firstName": "MCP",
+  "lastName": "User",
+  "enabled": true,
+  "emailVerified": true,
+  "credentials": [
+    {
+      "type": "password",
+      "value": "mcp",
+      "temporary": false
+    }
+  ]
+}
diff --git a/dev/config/kind/cluster.yaml b/dev/config/kind/cluster.yaml
index d78e802..fda1168 100644
--- a/dev/config/kind/cluster.yaml
+++ b/dev/config/kind/cluster.yaml
@@ -23,7 +23,7 @@ nodes:
         oidc-ca-file: /etc/kubernetes/pki/keycloak-ca.crt
   extraPortMappings:
   - containerPort: 80
-    hostPort: 8080
+    hostPort: 8000
     protocol: TCP
   - containerPort: 443
     hostPort: 8443
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 5fe8e16..5bd00ff 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -19,7 +19,7 @@ const (
 type StaticConfig struct {
 	DeniedResources []GroupVersionKind `toml:"denied_resources"`
 
-	LogLevel   int    `toml:"log_level,omitempty"`
+	LogLevel   int    `toml:"log_level,omitzero"`
 	Port       string `toml:"port,omitempty"`
 	SSEBaseURL string `toml:"sse_base_url,omitempty"`
 	KubeConfig string `toml:"kubeconfig,omitempty"`
@@ -70,13 +70,6 @@ type StaticConfig struct {
 	parsedClusterProviderConfigs map[string]ProviderConfig
 }
 
-func Default() *StaticConfig {
-	return &StaticConfig{
-		ListOutput: "table",
-		Toolsets:   []string{"core", "config", "helm"},
-	}
-}
-
 type GroupVersionKind struct {
 	Group   string `toml:"group"`
 	Version string `toml:"version"`
diff --git a/pkg/config/config_default.go b/pkg/config/config_default.go
new file mode 100644
index 0000000..febea70
--- /dev/null
+++ b/pkg/config/config_default.go
@@ -0,0 +1,43 @@
+package config
+
+import (
+	"bytes"
+
+	"github.com/BurntSushi/toml"
+)
+
+func Default() *StaticConfig {
+	defaultConfig := StaticConfig{
+		ListOutput: "table",
+		Toolsets:   []string{"core", "config", "helm"},
+	}
+	overrides := defaultOverrides()
+	mergedConfig := mergeConfig(defaultConfig, overrides)
+	return &mergedConfig
+}
+
+// HasDefaultOverrides indicates whether the internal defaultOverrides function
+// provides any overrides or an empty StaticConfig.
+func HasDefaultOverrides() bool {
+	overrides := defaultOverrides()
+	var buf bytes.Buffer
+	if err := toml.NewEncoder(&buf).Encode(overrides); err != nil {
+		// If marshaling fails, assume no overrides
+		return false
+	}
+	return len(bytes.TrimSpace(buf.Bytes())) > 0
+}
+
+// mergeConfig applies non-zero values from override to base using TOML serialization
+// and returns the merged StaticConfig.
+// In case of any error during marshalling or unmarshalling, it returns the base config unchanged.
+func mergeConfig(base, override StaticConfig) StaticConfig {
+	var overrideBuffer bytes.Buffer
+	if err := toml.NewEncoder(&overrideBuffer).Encode(override); err != nil {
+		// If marshaling fails, return base unchanged
+		return base
+	}
+
+	_, _ = toml.NewDecoder(&overrideBuffer).Decode(&base)
+	return base
+}
diff --git a/pkg/config/config_default_overrides.go b/pkg/config/config_default_overrides.go
new file mode 100644
index 0000000..70d065b
--- /dev/null
+++ b/pkg/config/config_default_overrides.go
@@ -0,0 +1,8 @@
+package config
+
+func defaultOverrides() StaticConfig {
+	return StaticConfig{
+		// IMPORTANT: this file is used to override default config values in downstream builds.
+		// This is intentionally left blank.
+	}
+}
diff --git a/pkg/kubernetes/nodes.go b/pkg/kubernetes/nodes.go
new file mode 100644
index 0000000..c6bb58f
--- /dev/null
+++ b/pkg/kubernetes/nodes.go
@@ -0,0 +1,48 @@
+package kubernetes
+
+import (
+	"context"
+	"fmt"
+)
+
+func (k *Kubernetes) NodeLog(ctx context.Context, name string, logPath string, tail int64) (string, error) {
+	// Use the node proxy API to access logs from the kubelet
+	// Common log paths:
+	// - /var/log/kubelet.log - kubelet logs
+	// - /var/log/kube-proxy.log - kube-proxy logs
+	// - /var/log/containers/ - container logs
+
+	if logPath == "" {
+		logPath = "kubelet.log"
+	}
+
+	// Build the URL for the node proxy logs endpoint
+	url := []string{"api", "v1", "nodes", name, "proxy", "logs", logPath}
+
+	// Query parameters for tail
+	params := make(map[string]string)
+	if tail > 0 {
+		params["tailLines"] = fmt.Sprintf("%d", tail)
+	}
+
+	req := k.manager.discoveryClient.RESTClient().
+		Get().
+		AbsPath(url...)
+
+	// Add tail parameter if specified
+	for key, value := range params {
+		req.Param(key, value)
+	}
+
+	result := req.Do(ctx)
+	if result.Error() != nil {
+		return "", fmt.Errorf("failed to get node logs: %w", result.Error())
+	}
+
+	rawData, err := result.Raw()
+	if err != nil {
+		return "", fmt.Errorf("failed to read node log response: %w", err)
+	}
+
+	return string(rawData), nil
+}
diff --git a/pkg/toolsets/core/nodes.go b/pkg/toolsets/core/nodes.go
new file mode 100644
index 0000000..5516dce
--- /dev/null
+++ b/pkg/toolsets/core/nodes.go
@@ -0,0 +1,79 @@
+package core
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/google/jsonschema-go/jsonschema"
+	"k8s.io/utils/ptr"
+
+	"github.com/containers/kubernetes-mcp-server/pkg/api"
+)
+
+func initNodes() []api.ServerTool {
+	return []api.ServerTool{
+		{Tool: api.Tool{
+			Name:        "node_log",
+			Description: "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+			InputSchema: &jsonschema.Schema{
+				Type: "object",
+				Properties: map[string]*jsonschema.Schema{
+					"name": {
+						Type:        "string",
+						Description: "Name of the node to get logs from",
+					},
+					"log_path": {
+						Type:        "string",
+						Description: "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+						Default:     api.ToRawMessage("kubelet.log"),
+					},
+					"tail": {
+						Type:        "integer",
+						Description: "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+						Default:     api.ToRawMessage(100),
+						Minimum:     ptr.To(float64(0)),
+					},
+				},
+				Required: []string{"name"},
+			},
+			Annotations: api.ToolAnnotations{
+				Title:           "Node: Log",
+				ReadOnlyHint:    ptr.To(true),
+				DestructiveHint: ptr.To(false),
+				IdempotentHint:  ptr.To(false),
+				OpenWorldHint:   ptr.To(true),
+			},
+		}, Handler: nodesLog},
+	}
+}
+
+func nodesLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
+	name := params.GetArguments()["name"]
+	if name == nil {
+		return api.NewToolCallResult("", errors.New("failed to get node log, missing argument name")), nil
+	}
+	logPath := params.GetArguments()["log_path"]
+	if logPath == nil {
+		logPath = "kubelet.log"
+	}
+	tail := params.GetArguments()["tail"]
+	var tailInt int64
+	if tail != nil {
+		// Convert to int64 - safely handle both float64 (JSON number) and int types
+		switch v := tail.(type) {
+		case float64:
+			tailInt = int64(v)
+		case int: case int64:
+			tailInt = int64(v)
+		default:
+			return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: expected integer, got %T", tail)), nil
+		}
+	}
+	ret, err := params.NodeLog(params, name.(string), logPath.(string), tailInt)
+	if err != nil {
+		return api.NewToolCallResult("", fmt.Errorf("failed to get node log for %s: %v", name, err)), nil
+	} else if ret == "" {
+		ret = fmt.Sprintf("The node %s has not logged any message yet or the log file is empty", name)
+	}
+	return api.NewToolCallResult(ret, nil), nil
+}
diff --git a/pkg/toolsets/core/toolset.go b/pkg/toolsets/core/toolset.go
index 9f88c7a..dfd61f4 100644
--- a/pkg/toolsets/core/toolset.go
+++ b/pkg/toolsets/core/toolset.go
@@ -24,6 +24,7 @@ func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
 	return slices.Concat(
 		initEvents(),
 		initNamespaces(o),
+		initNodes(),
 		initPods(),
 		initResources(o),
 	)



Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index d0e8772..afdde19 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -174,6 +174,49 @@ func (s *ConfigSuite) TestReadConfigValidPreservesDefaultsForMissingFields() {
 	})
 }
 
+func (s *ConfigSuite) TestMergeConfig() {
+	base := StaticConfig{
+		ListOutput: "table",
+		Toolsets:   []string{"core", "config", "helm"},
+		Port:       "8080",
+	}
+	s.Run("merges override values on top of base", func() {
+		override := StaticConfig{
+			ListOutput: "json",
+			Port:       "9090",
+		}
+
+		result := mergeConfig(base, override)
+
+		s.Equal("json", result.ListOutput, "ListOutput should be overridden")
+		s.Equal("9090", result.Port, "Port should be overridden")
+	})
+
+	s.Run("preserves base values when override is empty", func() {
+		override := StaticConfig{}
+
+		result := mergeConfig(base, override)
+
+		s.Equal("table", result.ListOutput, "ListOutput should be preserved from base")
+		s.Equal([]string{"core", "config", "helm"}, result.Toolsets, "Toolsets should be preserved from base")
+		s.Equal("8080", result.Port, "Port should be preserved from base")
+	})
+
+	s.Run("handles partial overrides", func() {
+		override := StaticConfig{
+			Toolsets: []string{"custom"},
+			ReadOnly: true,
+		}
+
+		result := mergeConfig(base, override)
+
+		s.Equal("table", result.ListOutput, "ListOutput should be preserved from base")
+		s.Equal([]string{"custom"}, result.Toolsets, "Toolsets should be overridden")
+		s.Equal("8080", result.Port, "Port should be preserved from base since override doesn't specify it")
+		s.True(result.ReadOnly, "ReadOnly should be overridden to true")
+	})
+}
+
 func TestConfig(t *testing.T) {
 	suite.Run(t, new(ConfigSuite))
 }
diff --git a/pkg/mcp/nodes_test.go b/pkg/mcp/nodes_test.go
new file mode 100644
index 0000000..b991577
--- /dev/null
+++ b/pkg/mcp/nodes_test.go
@@ -0,0 +1,66 @@
+package mcp
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestNodeLog(t *testing.T) {
+	testCase(t, func(c *mcpContext) {
+		c.withEnvTest()
+
+		// Create test node
+		kubernetesAdmin := c.newKubernetesClient()
+		node := &corev1.Node{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "test-node-log",
+			},
+			Status: corev1.NodeStatus{
+				Addresses: []corev1.NodeAddress{
+					{Type: corev1.NodeInternalIP, Address: "192.168.1.10"},
+				},
+			},
+		}
+
+		_, _ = kubernetesAdmin.CoreV1().Nodes().Create(c.ctx, node, metav1.CreateOptions{})
+
+		// Test node_log tool
+		toolResult, err := c.callTool("node_log", map[string]interface{}{
+			"name": "test-node-log",
+		})
+
+		t.Run("node_log returns successfully or with expected error", func(t *testing.T) {
+			if err != nil {
+				t.Fatalf("call tool failed: %v", err)
+			}
+			// Node logs might not be available in test environment
+			// We just check that the tool call completes
+			if toolResult.IsError {
+				content := toolResult.Content[0].(mcp.TextContent).Text
+				// Expected error messages in test environment
+				if !strings.Contains(content, "failed to get node logs") &&
+					!strings.Contains(content, "not logged any message yet") {
+					t.Logf("tool returned error (expected in test environment): %v", content)
+				}
+			}
+		})
+	})
+}
+
+func TestNodeLogMissingArguments(t *testing.T) {
+	testCase(t, func(c *mcpContext) {
+		c.withEnvTest()
+
+		t.Run("node_log requires name", func(t *testing.T) {
+			toolResult, err := c.callTool("node_log", map[string]interface{}{})
+
+			if err == nil && !toolResult.IsError {
+				t.Fatal("expected error when name is missing")
+			}
+		})
+	})
+}
diff --git a/pkg/mcp/testdata/toolsets-core-tools.json b/pkg/mcp/testdata/toolsets-core-tools.json
index 43680da..5039cf2 100644
--- a/pkg/mcp/testdata/toolsets-core-tools.json
+++ b/pkg/mcp/testdata/toolsets-core-tools.json
@@ -33,6 +33,40 @@
     },
     "name": "namespaces_list"
   },
+  {
+    "annotations": {
+      "title": "Node: Log",
+      "readOnlyHint": true,
+      "destructiveHint": false,
+      "idempotentHint": false,
+      "openWorldHint": true
+    },
+    "description": "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+    "inputSchema": {
+      "type": "object",
+      "properties": {
+        "log_path": {
+          "default": "kubelet.log",
+          "description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+          "type": "string"
+        },
+        "name": {
+          "description": "Name of the node to get logs from",
+          "type": "string"
+        },
+        "tail": {
+          "default": 100,
+          "description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+          "minimum": 0,
+          "type": "integer"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "name": "node_log"
+  },
   {
     "annotations": {
       "title": "Pods: Delete",
diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json
index 97af6fb..cec61b9 100644
--- a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json
+++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json
@@ -195,6 +195,48 @@
     },
     "name": "namespaces_list"
   },
+  {
+    "annotations": {
+      "title": "Node: Log",
+      "readOnlyHint": true,
+      "destructiveHint": false,
+      "idempotentHint": false,
+      "openWorldHint": true
+    },
+    "description": "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+    "inputSchema": {
+      "type": "object",
+      "properties": {
+        "context": {
+          "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set",
+          "enum": [
+            "extra-cluster",
+            "fake-context"
+          ],
+          "type": "string"
+        },
+        "log_path": {
+          "default": "kubelet.log",
+          "description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+          "type": "string"
+        },
+        "name": {
+          "description": "Name of the node to get logs from",
+          "type": "string"
+        },
+        "tail": {
+          "default": 100,
+          "description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+          "minimum": 0,
+          "type": "integer"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "name": "node_log"
+  },
   {
     "annotations": {
       "title": "Pods: Delete",
diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
index 861a1b5..7f1f91d 100644
--- a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
+++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
@@ -175,6 +175,44 @@
     },
     "name": "namespaces_list"
   },
+  {
+    "annotations": {
+      "title": "Node: Log",
+      "readOnlyHint": true,
+      "destructiveHint": false,
+      "idempotentHint": false,
+      "openWorldHint": true
+    },
+    "description": "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+    "inputSchema": {
+      "type": "object",
+      "properties": {
+        "context": {
+          "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set",
+          "type": "string"
+        },
+        "log_path": {
+          "default": "kubelet.log",
+          "description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+          "type": "string"
+        },
+        "name": {
+          "description": "Name of the node to get logs from",
+          "type": "string"
+        },
+        "tail": {
+          "default": 100,
+          "description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+          "minimum": 0,
+          "type": "integer"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "name": "node_log"
+  },
   {
     "annotations": {
       "title": "Pods: Delete",
diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json
index b501894..066eee0 100644
--- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json
+++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json
@@ -139,6 +139,40 @@
     },
     "name": "namespaces_list"
   },
+  {
+    "annotations": {
+      "title": "Node: Log",
+      "readOnlyHint": true,
+      "destructiveHint": false,
+      "idempotentHint": false,
+      "openWorldHint": true
+    },
+    "description": "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+    "inputSchema": {
+      "type": "object",
+      "properties": {
+        "log_path": {
+          "default": "kubelet.log",
+          "description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+          "type": "string"
+        },
+        "name": {
+          "description": "Name of the node to get logs from",
+          "type": "string"
+        },
+        "tail": {
+          "default": 100,
+          "description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+          "minimum": 0,
+          "type": "integer"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "name": "node_log"
+  },
   {
     "annotations": {
       "title": "Pods: Delete",
diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json
index 7b9f471..17b5e7e 100644
--- a/pkg/mcp/testdata/toolsets-full-tools.json
+++ b/pkg/mcp/testdata/toolsets-full-tools.json
@@ -139,6 +139,40 @@
     },
     "name": "namespaces_list"
   },
+  {
+    "annotations": {
+      "title": "Node: Log",
+      "readOnlyHint": true,
+      "destructiveHint": false,
+      "idempotentHint": false,
+      "openWorldHint": true
+    },
+    "description": "Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet",
+    "inputSchema": {
+      "type": "object",
+      "properties": {
+        "log_path": {
+          "default": "kubelet.log",
+          "description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
+          "type": "string"
+        },
+        "name": {
+          "description": "Name of the node to get logs from",
+          "type": "string"
+        },
+        "tail": {
+          "default": 100,
+          "description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
+          "minimum": 0,
+          "type": "integer"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "name": "node_log"
+  },
   {
     "annotations": {
       "title": "Pods: Delete",
diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go
index 527b1e2..d81392a 100644
--- a/pkg/mcp/toolsets_test.go
+++ b/pkg/mcp/toolsets_test.go
@@ -65,6 +65,9 @@ func (s *ToolsetsSuite) TestNoToolsets() {
 }
 
 func (s *ToolsetsSuite) TestDefaultToolsetsTools() {
+	if configuration.HasDefaultOverrides() {
+		s.T().Skip("Skipping test because default configuration overrides are present (this is a downstream fork)")
+	}
 	s.Run("Default configuration toolsets", func() {
 		s.InitMcpClient()
 		tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{})
@@ -82,6 +85,9 @@ func (s *ToolsetsSuite) TestDefaultToolsetsTools() {
 }
 
 func (s *ToolsetsSuite) TestDefaultToolsetsToolsInOpenShift() {
+	if configuration.HasDefaultOverrides() {
+		s.T().Skip("Skipping test because default configuration overrides are present (this is a downstream fork)")
+	}
 	s.Run("Default configuration toolsets in OpenShift", func() {
 		s.Handle(&test.InOpenShiftHandler{})
 		s.InitMcpClient()
@@ -100,6 +106,9 @@ func (s *ToolsetsSuite) TestDefaultToolsetsToolsInOpenShift() {
 }
 
 func (s *ToolsetsSuite) TestDefaultToolsetsToolsInMultiCluster() {
+	if configuration.HasDefaultOverrides() {
+		s.T().Skip("Skipping test because default configuration overrides are present (this is a downstream fork)")
+	}
 	s.Run("Default configuration toolsets in multi-cluster (with 11 clusters)", func() {
 		kubeconfig := s.Kubeconfig()
 		for i := 0; i < 10; i++ {
@@ -123,6 +132,9 @@ func (s *ToolsetsSuite) TestDefaultToolsetsToolsInMultiCluster() {
 }
 
 func (s *ToolsetsSuite) TestDefaultToolsetsToolsInMultiClusterEnum() {
+	if configuration.HasDefaultOverrides() {
+		s.T().Skip("Skipping test because default configuration overrides are present (this is a downstream fork)")
+	}
 	s.Run("Default configuration toolsets in multi-cluster (with 2 clusters)", func() {
 		kubeconfig := s.Kubeconfig()
 		// Add additional cluster to force multi-cluster behavior with enum parameter