Skip to content

Add lazy tools/list discovery for Kubernetes sources - #852

Open
beersoccer wants to merge 2 commits into
stacklok:mainfrom
beersoccer:feat/lazy-tool-discovery
Open

beersoccer wants to merge 2 commits into
stacklok:mainfrom
beersoccer:feat/lazy-tool-discovery

Conversation

@beersoccer

Copy link
Copy Markdown

Summary

When a MCPServer, VirtualMCPServer, or MCPRemoteProxy CRD is exported to the registry (toolhive.stacklok.dev/registry-export: "true") but carries no tool-definitions annotation, the K8s controller now automatically connects to the proxy's registry-url and calls tools/list at reconcile time, storing the discovered tools in the database.

This removes the need for operators to manually annotate every tool on each CRD — the registry self-populates from the running proxy and stays up-to-date on every reconcile.

Changes

  • internal/kubernetes/discover.go (new) — Streamable-HTTP MCP client with SSE fallback; injects OAuth2 Bearer token when TokenSource is configured
  • internal/kubernetes/oauth.go (new) — golang.org/x/oauth2/clientcredentials wrapper; nil TokenSource means anonymous (backward compatible)
  • internal/config/config.go — New discoverTimeout and discoverOAuth2 fields on KubernetesConfig; client secret read from file path per project secrets policy
  • internal/kubernetes/types.goextractServer, extractVirtualMCPServer, extractMCPRemoteProxy fall back to discoverTools when tool-definitions annotation is absent
  • internal/kubernetes/controller.goextractorFunc extended with ctx and tokenSource
  • internal/app/builder.go — Constructs TokenSource from config and injects it via WithDiscoverTokenSource
  • docs/lazy-tool-discovery.md (new) — Feature overview, OAuth2 setup, Helm deployment guide, annotation reference
  • docs/configuration.mddiscoverOAuth2 field table added to Kubernetes source section
  • deploy/charts/.../values.yaml — Example discoverOAuth2 block added to Helm values comments

Behavior

tool-definitions annotation registry-url annotation Result
✅ present any annotation used directly — lazy discovery not triggered
❌ absent ✅ present lazy discoverytools/list called at reconcile time
❌ absent ❌ absent empty — no HTTP connection made

Discovery failures are non-fatal: a warning is logged and the entry is still registered; retry happens on the next reconcile.

OAuth2 support

Production MCP proxies typically enforce Cedar authorization. The new discoverOAuth2 config block enables client_credentials authentication:

sources:
  - name: k8s-source
    kubernetes:
      namespaces: ["toolhive-system"]
      discoverTimeout: "15s"
      discoverOAuth2:
        tokenUrl: https://keycloak.example.com/realms/toolhive/protocol/openid-connect/token
        clientId: registry-server-discovery
        clientSecretFile: /var/run/secrets/toolhive/discover-oauth2/client-secret
        scopes: ["openid"]

When discoverOAuth2 is omitted, discovery runs anonymously (backward compatible).

Testing

  • task lint-fix — 0 issues
  • task test PACKAGES=./internal/kubernetes/... — all pass, no data race
  • task test PACKAGES=./internal/config/... — all pass
  • Manually verified against a Cedar-protected ToolHive deployment: tools/list returns discovered tools which appear in /registry/{name}/v0.1/servers

@beersoccer
beersoccer force-pushed the feat/lazy-tool-discovery branch from 59b7e63 to 140b98a Compare July 23, 2026 08:51
WHY: Manually annotating every tool on each MCPServer/VirtualMCPServer/
MCPRemoteProxy CRD is error-prone and goes stale as proxies evolve. The
registry should self-populate tool metadata by querying the running proxy
instead of relying on operators to keep annotations in sync.

Adds `discoverTools` to the K8s controller reconcile path: when a CRD
is exported (`registry-export: "true"`) but carries no `tool-definitions`
annotation, the controller opens a Streamable-HTTP (SSE fallback) MCP
connection to the `registry-url` endpoint and calls `tools/list`. The
result is written to the DB via the existing sync writer; no K8s patch
is performed and no new RBAC is required.

Adds `discoverOAuth2` to `KubernetesConfig` so that a client_credentials
grant can be configured for environments where MCP proxies enforce Cedar
authorization. The client secret is read from a file path per `secrets.md`
§1; tokens are cached and auto-refreshed by `x/oauth2/clientcredentials`.
Discovery failures are non-fatal — a warning is logged and `ToolDefinitions`
is left empty, with retry on the next reconcile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: beersoccer <beersoccer@gmail.com>
@beersoccer
beersoccer force-pushed the feat/lazy-tool-discovery branch from 140b98a to 11437d4 Compare July 23, 2026 09:04
@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.85%. Comparing base (91b63fc) to head (5143ee1).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #852      +/-   ##
==========================================
- Coverage   62.07%   60.85%   -1.22%     
==========================================
  Files         109       25      -84     
  Lines       10724     4665    -6059     
==========================================
- Hits         6657     2839    -3818     
+ Misses       3482     1472    -2010     
+ Partials      585      354     -231     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

CVE-2026-39822 (CVSS 7.8 HIGH) affects Go 1.26.0 through 1.26.4 via
an os.Root symlink escape on Unix, fixed in Go 1.26.5 (2026-07-07).
Raising the go directive ensures the module cannot be built with a
vulnerable toolchain and clears the Grype HIGH finding that was
blocking CI. The vulnerability predates this PR and is present in
the upstream main branch as well.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@beersoccer

Copy link
Copy Markdown
Author

The second commit (5143ee1) bumps the go directive in go.mod from 1.26.0 to 1.26.5 to resolve the Grype HIGH finding that blocked CI.

CVE-2026-39822 (CVSS 7.8 HIGH) — os.Root symlink escape on Unix — affects Go 1.26.0 through 1.26.4 and was fixed in Go 1.26.5 (released 2026-07-07). This vulnerability predates this PR; the upstream main branch is equally affected since it also declares go 1.26.0. Raising the directive to 1.26.5 ensures the module cannot be compiled with a vulnerable toolchain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants