Skip to content

feat: add Base14 Scout toolset integration - #1860

Closed
nimisha-gj wants to merge 14 commits into
HolmesGPT:masterfrom
base-14:master
Closed

nimisha-gj wants to merge 14 commits into
HolmesGPT:masterfrom
base-14:master

Conversation

@nimisha-gj

@nimisha-gj nimisha-gj commented Mar 31, 2026 •

Copy link
Copy Markdown

Summary

  • Add native base14/scout toolset for the Base14 Scout observability platform
  • Scout provides unified access to services, traces, logs, metrics, and alerts via MCP
  • Extends RemoteMCPToolset — tools are auto-discovered from the Scout MCP server at startup

Changes

New files:

  • holmes/plugins/toolsets/scout/ — ScoutConfig, ScoutToolset, LLM investigation instructions
  • tests/test_scout_toolset.py — 9 unit tests
  • docs/data-sources/builtin-toolsets/base14-scout.md — full documentation page
  • images/integration_logos/base14-scout-icon.png — logo

Modified files:

  • holmes/plugins/toolsets/__init__.py — register ScoutToolset in plugin loader
  • holmes/core/toolset_manager.py — fix eager init for RemoteMCPToolset subclasses (isinstance check)
  • README.md — add Scout to data sources table
  • docs/data-sources/builtin-toolsets/index.md — add grid card
  • docs/data-sources/builtin-toolsets/.nav.yml — add nav entry
  • docs/why-holmesgpt.md — add Scout to integration categories

Authentication

Supports two auth modes:

  • client_id + client_secret — OAuth client_credentials grant (auto-discovers token endpoint from Scout's well-known URL)
  • api_token — direct Bearer token

Toolset Manager Fix

RemoteMCPToolset subclasses (like ScoutToolset) are registered as ToolsetType.BUILTIN by load_builtin_toolsets(), which means they were lazily initialized from cache — tools weren't rediscovered. Added isinstance(toolset, RemoteMCPToolset) check alongside the existing toolset.type == ToolsetType.MCP check so any MCP-based toolset gets eagerly initialized.

Checklist

  • Toolset has unit tests
  • Toolset has both ask_holmes and investigate evals (requires live Scout instance in CI — will add once CI access is coordinated)
  • Toolset has documentation
  • Toolset has config_classes defined with Field annotations for JSON Schema generation
  • Toolset does a live health check in addition to checking for correct configuration

Test plan

  • Unit tests pass: poetry run pytest tests/test_scout_toolset.py -v --no-cov
  • Toolset loads and discovers all 14 tools from Scout MCP server
  • OAuth client_credentials auth flow works end-to-end
  • Full investigation completed: 3 firing alerts → 11 tool calls → correct RCA (payment service unreachable due to feature flag)

Summary by CodeRabbit

  • New Features

    • Added Base14 Scout as a built-in data source integration, enabling access to services, traces, logs, metrics, and alerts via MCP.
    • Support for both static API token and OAuth client credentials authentication.
  • Documentation

    • Added comprehensive Base14 Scout setup and configuration guide.
    • Updated data sources index and reference materials.

nimishgj added 13 commits March 27, 2026 16:14
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
…override

Signed-off-by: nimishgj <nimishgj444@gmail.com>
…ubclasses

- Replace username/password auth with client_id/client_secret (client_credentials grant)
- Fix toolset_manager to eagerly init RemoteMCPToolset subclasses via isinstance check
  so Scout tools are rediscovered from cache instead of being empty
- Update tests to match new auth fields

Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
Signed-off-by: nimishgj <nimishgj444@gmail.com>
…nology

Signed-off-by: nimishgj <nimishgj444@gmail.com>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@netlify

netlify Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for holmes-docs ready!

Name Link
🔨 Latest commit d1f0772
🔍 Latest deploy log https://app.netlify.com/projects/holmes-docs/deploys/69cb80b20d68d40008e19128
😎 Deploy Preview https://deploy-preview-1860--holmes-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces a new Base14 Scout observability platform integration to HolmesGPT as a built-in Remote MCP toolset. Changes include documentation updates describing the integration, core toolset manager modifications to handle RemoteMCPToolset instances, the Scout toolset implementation with OAuth client-credentials support, LLM instruction template, and test coverage.

Changes

Cohort / File(s) Summary
Documentation Updates
README.md, docs/why-holmesgpt.md, docs/data-sources/builtin-toolsets/index.md, docs/data-sources/builtin-toolsets/.nav.yml, docs/data-sources/builtin-toolsets/base14-scout.md
Added Base14 Scout to data sources table, navigation, toolset index, and why-holmesgpt page. New documentation page covers credential requirements (API token or OAuth), environment/YAML configuration, authentication behavior, MCP tool discovery, and example use cases.
Core Toolset Management
holmes/core/toolset_manager.py
Updated load_toolset_with_status cache-hit and eager-initialization logic to treat RemoteMCPToolset instances as MCP toolsets, ensuring they are included in enabled toolsets and initialized eagerly for tool discovery.
Toolset Plugin Registration
holmes/plugins/toolsets/__init__.py
Added import and instantiation of ScoutToolset to load_python_toolsets() function, registering it as a built-in Python toolset.
Scout Toolset Implementation
holmes/plugins/toolsets/scout/toolset_scout.py, holmes/plugins/toolsets/scout/toolset_scout.jinja2
Introduced ScoutConfig with API URL, authentication (token or OAuth credentials), and SSL verification settings. Implemented _obtain_token_via_client_credentials() for OpenID Connect OAuth flow. Created ScoutToolset subclassing RemoteMCPToolset with prerequisite validation, environment variable resolution, and MCP session configuration with Bearer token authorization. Added Jinja2 template defining LLM investigation strategy with discovery-before-query patterns and time-window constraints.
Test Coverage
tests/test_scout_toolset.py
Added pytest tests validating ScoutConfig construction (with token or OAuth credentials), required api_url, default values (mode="streamable-http", verify_ssl=True), ScoutToolset metadata (name, disabled-by-default), and prerequisite evaluation logic for missing credentials.

Sequence Diagram

sequenceDiagram
    participant Scout as ScoutToolset
    participant OAuth as OAuth/OIDC Server
    participant MCP as MCP Server
    participant API as Base14 Scout API
    
    Scout->>Scout: Load config from env vars & YAML
    alt Using OAuth credentials
        Scout->>OAuth: Fetch .well-known/openid-configuration
        OAuth-->>Scout: OAuth metadata (token_endpoint)
        Scout->>OAuth: POST client_credentials grant
        OAuth-->>Scout: access_token
        Scout->>Scout: Store Bearer token
    else Using API token
        Scout->>Scout: Use api_token directly
    end
    
    Scout->>MCP: Connect with Bearer Authorization header
    MCP-->>Scout: Connection established
    Scout->>MCP: Discover available tools
    MCP-->>Scout: Tool definitions (health, services, spans, logs, etc.)
    Scout->>API: Ready to execute MCP tools
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested reviewers

  • arikalon1
  • moshemorad
  • RoiGlinik
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Base14 Scout toolset integration' clearly and concisely summarizes the main change: adding a new Base14 Scout toolset. It is specific, directly related to the primary purpose of the PR, and follows conventional commit format.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
docs/data-sources/builtin-toolsets/base14-scout.md (1)

99-118: Consider removing the detailed tools list.

Per coding guidelines, toolset documentation should not list what a toolset/integration can do because "users discover capabilities by using Holmes, and feature lists become stale quickly." The tools are auto-discovered from the MCP server, so maintaining this list creates a documentation maintenance burden.

Consider simplifying to:

 ## Available Tools
 
-All tools are auto-discovered from the Scout MCP server. The following tools are typically available:
-
-| Tool | Description |
-|------|-------------|
-| `get_system_health_summary` | Full system overview — alerts, services, and performance stats |
-| `list_services` | Discover all known services |
-| `get_service_topology` | Service-to-service dependency map |
-| `get_service_dependencies` | Upstream/downstream dependencies for a specific service |
-| `get_service_profile` | Error rates and latency stats per relationship |
-| `get_service_metrics` | Metrics emitted by a service |
-| `get_last_n_alerts` | Recent Grafana alert state transitions |
-| `discover_spans` | Available span types and attributes for a service |
-| `discover_logs` | Available log attributes and severity levels |
-| `discover_metrics` | Available metrics, types, and dimensions |
-| `query_traces` | Query traces with filtering by span name, status, attributes |
-| `query_trace_by_id` | Full trace detail with all spans, events, and links |
-| `query_logs` | Query logs with filtering by severity, body, attributes |
-| `query_metrics` | Query metric values with aggregation and grouping |
+All tools are auto-discovered from the Scout MCP server at startup. Run `holmes ask "What tools are available?"` to see the current list.

As per coding guidelines: "Don't list what a toolset/integration can do in documentation - users discover capabilities by using Holmes, and feature lists become stale quickly"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/data-sources/builtin-toolsets/base14-scout.md` around lines 99 - 118,
Remove the detailed tools table under the "## Available Tools" section (which
lists entries like `get_system_health_summary`, `list_services`, `query_traces`,
etc.) and replace it with a single concise paragraph stating that tools are
auto-discovered from the Scout MCP server and that users should discover
available capabilities via Holmes; include the guideline note: "Don't list what
a toolset/integration can do in documentation - users discover capabilities by
using Holmes, and feature lists become stale quickly." Ensure the header "##
Available Tools" remains but the long table is removed to avoid stale
documentation.
tests/test_scout_toolset.py (2)

27-29: Use specific exception type instead of generic Exception.

The test should catch the specific exception raised by Pydantic validation (ValidationError) rather than a blind Exception. This ensures the test validates the correct failure mode.

♻️ Proposed fix
+from pydantic import ValidationError
+
 class TestScoutConfig:
     ...
     def test_api_url_required(self):
-        with pytest.raises(Exception):
+        with pytest.raises(ValidationError):
             ScoutConfig(api_token="test-token")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_scout_toolset.py` around lines 27 - 29, Change the test to assert
the specific Pydantic validation error: in test_api_url_required replace the
generic with pytest.raises(ValidationError) so the test catches the Pydantic
failure from constructing ScoutConfig without api_url; import ValidationError
from pydantic and ensure the test references test_api_url_required and
ScoutConfig to locate the change.

70-71: Prefix unused variable with underscore.

The msg variable is unpacked but never used. Prefix it with an underscore to indicate it's intentionally ignored.

♻️ Proposed fix
-        ok, msg = toolset.prerequisites_callable({})
+        ok, _msg = toolset.prerequisites_callable({})
         assert ok is False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_scout_toolset.py` around lines 70 - 71, The test unpacks the
return of toolset.prerequisites_callable({}) into ok, msg but never uses msg;
update the unpacking to prefix the unused variable with an underscore (e.g., ok,
_msg = toolset.prerequisites_callable({})) in tests/test_scout_toolset.py so the
intent is clear and linters won’t flag the unused variable; keep the call to
toolset.prerequisites_callable unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/plugins/toolsets/scout/toolset_scout.py`:
- Around line 70-75: The code currently builds a Keycloak-specific token URL
(`token_url = f"{auth_server_url}/protocol/openid-connect/token"`) which breaks
OIDC interoperability; change the logic in the function that reads `metadata`
(using `well_known_url`, `auth_servers`, `auth_server_url`) to derive
`token_url` from the OIDC discovery document instead: prefer
metadata.get("token_endpoint") (or metadata["token_endpoint"] when present) and
only fall back to a computed URL if absolutely necessary; update uses of
`token_url` accordingly and remove the hardcoded Keycloak path so the function
truly "derives the token endpoint" per OpenID Connect discovery.

---

Nitpick comments:
In `@docs/data-sources/builtin-toolsets/base14-scout.md`:
- Around line 99-118: Remove the detailed tools table under the "## Available
Tools" section (which lists entries like `get_system_health_summary`,
`list_services`, `query_traces`, etc.) and replace it with a single concise
paragraph stating that tools are auto-discovered from the Scout MCP server and
that users should discover available capabilities via Holmes; include the
guideline note: "Don't list what a toolset/integration can do in documentation -
users discover capabilities by using Holmes, and feature lists become stale
quickly." Ensure the header "## Available Tools" remains but the long table is
removed to avoid stale documentation.

In `@tests/test_scout_toolset.py`:
- Around line 27-29: Change the test to assert the specific Pydantic validation
error: in test_api_url_required replace the generic with
pytest.raises(ValidationError) so the test catches the Pydantic failure from
constructing ScoutConfig without api_url; import ValidationError from pydantic
and ensure the test references test_api_url_required and ScoutConfig to locate
the change.
- Around line 70-71: The test unpacks the return of
toolset.prerequisites_callable({}) into ok, msg but never uses msg; update the
unpacking to prefix the unused variable with an underscore (e.g., ok, _msg =
toolset.prerequisites_callable({})) in tests/test_scout_toolset.py so the intent
is clear and linters won’t flag the unused variable; keep the call to
toolset.prerequisites_callable unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6e20031-f0fe-45b8-96f1-9edc583feb14

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef4643 and c54aed6.

⛔ Files ignored due to path filters (1)
  • images/integration_logos/base14-scout-icon.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • README.md
  • docs/data-sources/builtin-toolsets/.nav.yml
  • docs/data-sources/builtin-toolsets/base14-scout.md
  • docs/data-sources/builtin-toolsets/index.md
  • docs/why-holmesgpt.md
  • holmes/core/toolset_manager.py
  • holmes/plugins/toolsets/__init__.py
  • holmes/plugins/toolsets/scout/__init__.py
  • holmes/plugins/toolsets/scout/toolset_scout.jinja2
  • holmes/plugins/toolsets/scout/toolset_scout.py
  • tests/test_scout_toolset.py

Comment on lines +70 to +75
auth_servers = metadata.get("authorization_servers", [])
if not auth_servers:
raise ValueError(f"No authorization_servers found in well-known metadata at {well_known_url}")

auth_server_url = auth_servers[0].rstrip("/")
token_url = f"{auth_server_url}/protocol/openid-connect/token"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the standard OAuth 2.0 OIDC discovery endpoint for finding the token_endpoint?

💡 Result:

The standard OAuth 2.0 OIDC discovery endpoint for finding the token_endpoint is the OpenID Provider Configuration endpoint, located at /.well-known/openid-configuration appended to the Issuer URL (e.g., https://example.com/.well-known/openid-configuration). This JSON document includes the token_endpoint field, as defined in the OpenID Connect Discovery 1.0 specification.

Citations:


🏁 Script executed:

cat -n holmes/plugins/toolsets/scout/toolset_scout.py | head -100

Repository: HolmesGPT/holmesgpt

Length of output: 4069


🏁 Script executed:

# Get the full context of the _obtain_token_via_client_credentials function
sed -n '50,90p' holmes/plugins/toolsets/scout/toolset_scout.py

Repository: HolmesGPT/holmesgpt

Length of output: 1647


🏁 Script executed:

# Check what imports are used for HTTP requests
head -40 holmes/plugins/toolsets/scout/toolset_scout.py

Repository: HolmesGPT/holmesgpt

Length of output: 1195


Hardcoded token endpoint breaks compatibility with non-Keycloak OAuth providers.

Line 75 constructs the token URL using the Keycloak-specific path /protocol/openid-connect/token, which won't work with Auth0, Okta, Azure AD, or other OAuth2 providers. The function's docstring claims to "derive the token endpoint" but instead hardcodes it.

Per the OpenID Connect Discovery specification, the token endpoint should be discovered from the OIDC configuration document at {auth_server}/.well-known/openid-configuration, which includes the standard token_endpoint field.

Proposed fix using standard OIDC discovery
     auth_servers = metadata.get("authorization_servers", [])
     if not auth_servers:
         raise ValueError(f"No authorization_servers found in well-known metadata at {well_known_url}")

     auth_server_url = auth_servers[0].rstrip("/")
-    token_url = f"{auth_server_url}/protocol/openid-connect/token"
+    
+    # Discover token endpoint from OIDC configuration
+    oidc_config_url = f"{auth_server_url}/.well-known/openid-configuration"
+    logger.debug(f"Fetching OIDC configuration from {oidc_config_url}")
+    oidc_resp = http_requests.get(oidc_config_url, verify=verify_ssl, timeout=10)
+    oidc_resp.raise_for_status()
+    oidc_config = oidc_resp.json()
+    
+    token_url = oidc_config.get("token_endpoint")
+    if not token_url:
+        raise ValueError(f"No token_endpoint found in OIDC configuration at {oidc_config_url}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/scout/toolset_scout.py` around lines 70 - 75, The
code currently builds a Keycloak-specific token URL (`token_url =
f"{auth_server_url}/protocol/openid-connect/token"`) which breaks OIDC
interoperability; change the logic in the function that reads `metadata` (using
`well_known_url`, `auth_servers`, `auth_server_url`) to derive `token_url` from
the OIDC discovery document instead: prefer metadata.get("token_endpoint") (or
metadata["token_endpoint"] when present) and only fall back to a computed URL if
absolutely necessary; update uses of `token_url` accordingly and remove the
hardcoded Keycloak path so the function truly "derives the token endpoint" per
OpenID Connect discovery.

Signed-off-by: nimishgj <nimishgj444@gmail.com>
@nimisha-gj nimisha-gj closed this Mar 31, 2026
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