Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f72b926
Add OAuth authorization_code support for MCP servers
Avi-Robusta Mar 30, 2026
6ca8a8b
docs
Avi-Robusta Mar 30, 2026
e4641a9
Share OAuth tokens across MCP servers with same IdP, fix multi-audience
Avi-Robusta Mar 31, 2026
c3c08ad
Add CLI OAuth flow, auto-discovery, and Atlassian MCP support
Avi-Robusta Mar 31, 2026
8d63fba
Fix CLI/frontend mode detection: use request_context presence not hea…
Avi-Robusta Mar 31, 2026
4df8bdf
Defer DCR to runtime, send registration_endpoint to frontend
Avi-Robusta Mar 31, 2026
0a8b00c
Accept client_id from frontend DCR in encrypted OAuth payload
Avi-Robusta Mar 31, 2026
e07a33e
Derive persistent OAuth keypair from signing_key
Avi-Robusta Mar 31, 2026
08dff98
Persist OAuth keypair in CLI mode with machine-derived encryption
Avi-Robusta Mar 31, 2026
41afc35
Fix CLI OAuth: perform DCR when client_id is missing
Avi-Robusta Mar 31, 2026
6816441
Fix cache key mismatch after CLI DCR changes client_id
Avi-Robusta Mar 31, 2026
117bead
Add CLI OAuth flow tests with mocked browser/callback/DCR
Avi-Robusta Mar 31, 2026
7b3b305
Add cross-cluster OAuth token storage via DB
Avi-Robusta Apr 1, 2026
f87fb57
Handle read-only filesystem for DiskTokenStore gracefully
Avi-Robusta Apr 1, 2026
bd97823
Fix DB token lookup: use authorization_url as provider_name, not tool…
Avi-Robusta Apr 1, 2026
7df6ef1
Store refresh token expiry in DB, update DB on token refresh
Avi-Robusta Apr 1, 2026
028a723
Add user_id scoping to OAuth token storage and cache
Avi-Robusta Apr 6, 2026
d4e52e5
Add OAuth eval test (254_mcp_oauth) and consolidate OAuth unit tests
Avi-Robusta Apr 6, 2026
b00a2e7
Remove RSA encryption from OAuth flow, extract token management, add …
Avi-Robusta Apr 11, 2026
8e0fe28
Deduplicate OAuth token exchange, unify storage, skip manual tests in CI
Avi-Robusta Apr 12, 2026
68b2747
OAuth flow cleanup: move import to top-level, add OAuthCallback model…
Avi-Robusta Apr 12, 2026
722a213
Use decision field for OAuth in tool approvals instead of encrypted_t…
Avi-Robusta Apr 12, 2026
9e85d83
cleanup fixes
Avi-Robusta Apr 12, 2026
4898a60
working both flows
Avi-Robusta Apr 12, 2026
31d5b57
Remove dead code and clean up tool_executor OAuth naming
Avi-Robusta Apr 12, 2026
2636f91
refactoring tools
Avi-Robusta Apr 12, 2026
e08a4b8
Clean up OAuth tool executor: remove dead code, deduplicate logic, im…
Avi-Robusta Apr 12, 2026
39a1563
bugfix-approval not enabled param
Avi-Robusta Apr 12, 2026
5d0dbf5
Move OAuth config to RemoteMCPToolset.get_oauth_config(), clean up sy…
Avi-Robusta Apr 12, 2026
e11e4dc
accidental commit
Avi-Robusta Apr 12, 2026
b40c014
fixes
Avi-Robusta Apr 12, 2026
7ec91c9
refactoring
Avi-Robusta Apr 12, 2026
839bf69
refactoring
Avi-Robusta Apr 12, 2026
2e1d32d
refactoring
Avi-Robusta Apr 12, 2026
23e1e39
Refactor OAuth code out of toolset_mcp.py into dedicated modules
Avi-Robusta Apr 12, 2026
12721ef
Add error logging for OAuth callback 400/502 responses
Avi-Robusta Apr 12, 2026
568977a
Add request logging to OAuth callback to debug field forwarding
Avi-Robusta Apr 12, 2026
72063be
Debug: log token_url and redirect_uri in OAuth callback
Avi-Robusta Apr 12, 2026
90496bf
refactoring
Avi-Robusta Apr 12, 2026
319b13a
refactoring
Avi-Robusta Apr 12, 2026
9d43ad4
refactoring
Avi-Robusta Apr 13, 2026
597a19a
Replace OAuth section in remote-mcp-servers.md with link to dedicated…
Avi-Robusta Apr 13, 2026
35e779c
Merge master into oauth branch
Avi-Robusta Apr 13, 2026
b4e0473
Fix CLI OAuth port race condition in parallel test runs
Avi-Robusta Apr 13, 2026
f6d21b9
refactoring
Avi-Robusta Apr 13, 2026
1de70a8
Refactor OAuth: PR review fixes, per-user tool store, unified TokenStore
Avi-Robusta Apr 20, 2026
0adffea
Fix OAuth token refresh, tool display, and log verbosity
Avi-Robusta Apr 20, 2026
3ce3f14
Fix UI issue with missing toolset name for OAuth tools
Avi-Robusta Apr 20, 2026
2a8dd5e
Fix docs: OAuth tokens are persisted, not session-only
Avi-Robusta Apr 20, 2026
cc85823
Avoid DiskTokenStore init in cluster, move imports to top
Avi-Robusta Apr 20, 2026
7c07075
Remove redundant token check from prerequisites, clean up registratio…
Avi-Robusta Apr 20, 2026
dad9665
Merge master into oauth branch
Avi-Robusta Apr 20, 2026
276ffce
Address PR review comments: signing_key_hash filter, sweep→refresh re…
Avi-Robusta Apr 23, 2026
98e8520
Fix OAuth tools lost on toolset refresh — eager-load after executor r…
Avi-Robusta Apr 23, 2026
b481d81
Fix disk token file permissions race and guard toolset None in OAuth …
Avi-Robusta Apr 23, 2026
495eb21
Fix CLI OAuth: set cli_user identity, evict revoked tokens from persi…
Avi-Robusta Apr 23, 2026
835139e
Include oauth_config in toolset meta when syncing to DB
Avi-Robusta Apr 23, 2026
e168676
Fix shared MCPOAuthConfig.client_id mutation in OAuth callback
Avi-Robusta Apr 23, 2026
fde30a5
Fix test failures: rename sweep→refresh in tests, add oauth_connector…
Avi-Robusta Apr 23, 2026
5a5d070
Fix missing oauth_connector on manually-constructed ToolExecutor clon…
Avi-Robusta Apr 23, 2026
7aec83b
Merge branch 'master' into oauth
Avi-Robusta Apr 23, 2026
e60d1e4
Fix test ordering leak: reset singleton token manager store between t…
Avi-Robusta Apr 23, 2026
23a86a7
Fix test_server_endpoints mock: set num_llm_calls to int for otel rec…
Avi-Robusta Apr 23, 2026
e2f8063
Fix remaining test_server_endpoints mocks: set num_llm_calls on all M…
Avi-Robusta Apr 23, 2026
17da1b2
Fix read-only filesystem crash in OAuth callback: disable prerequisit…
Avi-Robusta Apr 23, 2026
f3b5d67
Remove cli_user: use __no_user__ fallback in connector instead
Avi-Robusta Apr 23, 2026
96de97b
Remove dead get_config_meta_for_toolset call in toolset sync
Avi-Robusta Apr 23, 2026
c351ff2
Only delete revoked tokens from disk store, not shared DB
Avi-Robusta Apr 23, 2026
bf3d340
Centralize CLI user_id handling via require_user_id and DEFAULT_CLI_USER
Avi-Robusta Apr 23, 2026
34e46fb
Remove unused get_config_meta_for_toolset function
Avi-Robusta Apr 23, 2026
3d4bd71
Trigger CI
Avi-Robusta Apr 23, 2026
b4f5e46
require_user_id returns None in server mode instead of raising
Avi-Robusta Apr 23, 2026
a0f0eb4
Retrigger Netlify deploy
Avi-Robusta Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
tmp/
config.yaml
checks.yaml
.claude/skills/dev-skills/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
11 changes: 11 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,14 @@ def responses():
rsps.add_passthru("https://www.google.com")
rsps.add_passthru("https://www.burgergooglenetworkspam.co.uk")
yield rsps


def pytest_collection_modifyitems(config, items):
"""Auto-skip tests marked with @pytest.mark.manual unless explicitly requested."""
markexpr = config.getoption("-m", default="")
if "manual" in markexpr:
return # User explicitly requested manual tests
skip_manual = pytest.mark.skip(reason="Manual test — run with: pytest -m manual")
for item in items:
if "manual" in item.keywords:
item.add_marker(skip_manual)
1 change: 1 addition & 0 deletions docs/data-sources/.nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ nav:
- Database Connectors: database-connectors.md
- Custom Toolsets: custom-toolsets.md
- MCP Servers: remote-mcp-servers.md
- OAuth MCP Servers (Alpha): oauth-mcp-servers.md
- HTTP Header Propagation: header-propagation.md
90 changes: 90 additions & 0 deletions docs/data-sources/oauth-mcp-servers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# OAuth MCP Servers

!!! warning "Alpha Feature"
OAuth MCP server support is a new feature and will experience rapid improvements. Configuration and behavior may change between releases.

Some MCP servers support OAuth-based authentication natively — you only need to set `oauth.enabled: true` and Holmes handles the rest. When Holmes connects to an OAuth-enabled MCP server, it automatically discovers the server's OAuth endpoints, opens a browser for login, and persists the token for future use.

## Setup

To add an OAuth MCP server, set `mode: streamable-http` and `oauth.enabled: true` in the server's config:

=== "Robusta CLI"

Set the `CUSTOM_TOOLSET_LOCATION` environment variable pointing to a YAML file with your MCP server configuration:

```bash
export CUSTOM_TOOLSET_LOCATION=/Users/.../custom_toolset.yaml
```

In that file, define your OAuth MCP servers:

```yaml
toolsets:
# ... your toolsets

mcp_servers:
my-server:
description: "Description of the MCP server"
config:
mode: streamable-http
url: https://example.com/mcp
oauth:
enabled: true
```

=== "Robusta Helm Chart with Platform"

Add the MCP servers to your `generated_values.yaml`. Make sure `enableHolmesGPT` is set to `true` and SaaS is enabled:

```yaml
holmes:
mcp_servers:
my-server:
description: "Description of the MCP server"
config:
mode: streamable-http
url: https://example.com/mcp
oauth:
enabled: true
```

```bash
helm upgrade robusta robusta/robusta --values=generated_values.yaml --set clusterName=<YOUR_CLUSTER_NAME>
```

## Example: Atlassian

=== "Robusta CLI"

```yaml
mcp_servers:
atlassian:
description: "Atlassian Jira + Confluence MCP server"
config:
mode: streamable-http
url: https://mcp.atlassian.com/v1/mcp
oauth:
enabled: true
```

=== "Robusta Helm Chart with Platform"

```yaml
holmes:
mcp_servers:
atlassian:
config:
mode: streamable-http
url: https://mcp.atlassian.com/v1/mcp
oauth:
enabled: true
```

## How It Works

1. Holmes detects that the MCP server has `oauth.enabled: true`
2. Holmes discovers the server's OAuth configuration automatically via the MCP protocol
3. The user is prompted to authenticate via their browser
4. After login, Holmes exchanges the authorization code for an access token
5. The token is persisted and refreshed automatically — users only need to authenticate once
4 changes: 4 additions & 0 deletions docs/data-sources/remote-mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ SSE transport is deprecated. Use `streamable-http` for new integrations.

The URL should end with `/sse`. If it doesn't, HolmesGPT will automatically append it.

## OAuth Authentication

For MCP servers that require OAuth authentication (e.g. Atlassian, Notion), see the dedicated [OAuth MCP Servers](oauth-mcp-servers.md) page.

## Advanced Configuration

**Dynamic Headers with Request Context**
Expand Down
3 changes: 3 additions & 0 deletions holmes/common/env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
RECOMMENDED_OPENAI_MODEL = "gpt-4.1"
RECOMMENDED_ANTHROPIC_MODEL = "anthropic/claude-opus-4-1-20250805"

# Default user_id for CLI mode (no authenticated user)
DEFAULT_CLI_USER = "__no_user__"

# Default model for HolmesGPT
DEFAULT_MODEL = RECOMMENDED_OPENAI_MODEL
FALLBACK_CONTEXT_WINDOW_SIZE = (
Expand Down
50 changes: 34 additions & 16 deletions holmes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
SecretStr,
)

from holmes.common.env_vars import ROBUSTA_CONFIG_PATH
from holmes.core.init_event import EventCallback, StatusEvent, StatusEventKind
from holmes.core.llm import DefaultLLM, LLMModelRegistry
from holmes.core.tools import PrerequisiteCacheMode, Toolset, ToolsetTag
Expand All @@ -41,10 +40,13 @@
from holmes.plugins.sources.prometheus.plugin import AlertManagerSource

from holmes.core.config import config_path_dir
from holmes.core.oauth_utils import eager_load_oauth_tools, preload_oauth_tokens, set_oauth_dal
from holmes.core.supabase_dal import SupabaseDal
from holmes.utils.definitions import RobustaConfig
from holmes.utils.pydantic_utils import RobustaBaseConfig, load_model_from_file



DEFAULT_CONFIG_LOCATION = os.path.join(config_path_dir, "config.yaml")


Expand Down Expand Up @@ -256,24 +258,28 @@ def load_from_env(cls):
result.log_useful_info()
return result

@staticmethod
def get_robusta_global_config_value(key: str) -> Optional[str]:
"""Read a value from Robusta's global_config. Returns None in CLI mode or on error."""
from holmes.common.env_vars import ROBUSTA_CONFIG_PATH

if not os.path.exists(ROBUSTA_CONFIG_PATH):
return None
try:
with open(ROBUSTA_CONFIG_PATH) as f:
yaml_content = yaml.safe_load(f)
config = RobustaConfig(**yaml_content)
return config.global_config.get(key)
except Exception:
logging.warning("Failed to load '%s' from Robusta config", key, exc_info=True)
return None

@staticmethod
def __get_cluster_name() -> Optional[str]:
config_file_path = ROBUSTA_CONFIG_PATH
env_cluster_name = os.environ.get("CLUSTER_NAME")
if env_cluster_name:
return env_cluster_name

if not os.path.exists(config_file_path):
logging.info(f"No robusta config in {config_file_path}")
return None

logging.info(f"loading config {config_file_path}")
with open(config_file_path) as file:
yaml_content = yaml.safe_load(file)
config = RobustaConfig(**yaml_content)
return config.global_config.get("cluster_name")

return None
return Config.get_robusta_global_config_value("cluster_name")

def get_runbook_catalog(self) -> Optional[RunbookCatalog]:
runbook_catalog = load_runbook_catalog(
Expand Down Expand Up @@ -354,6 +360,9 @@ def create_tool_executor(
tags = toolset_tag_filter or [ToolsetTag.CORE]
cache_key = self._executor_cache_key(tags, enable_all_toolsets_possible)

# Make DAL available for OAuth cross-cluster token storage
set_oauth_dal(dal)

if reuse_executor:
with self._executor_lock:
if (
Expand All @@ -373,6 +382,9 @@ def create_tool_executor(
executor = ToolExecutor(toolsets, on_event=on_event)
self._cached_tool_executor = executor
self._cached_executor_key = cache_key

preload_oauth_tokens()
eager_load_oauth_tools(executor)
return executor

toolsets = self.toolset_manager.prepare_toolsets(
Expand All @@ -382,7 +394,10 @@ def create_tool_executor(
prerequisite_cache=prerequisite_cache,
on_event=on_event,
)
return ToolExecutor(toolsets, on_event=on_event)
preload_oauth_tokens()
executor = ToolExecutor(toolsets, on_event=on_event)
eager_load_oauth_tools(executor)
return executor

def refresh_tool_executor(
self,
Expand Down Expand Up @@ -429,7 +444,10 @@ def refresh_tool_executor(

if changes:
with self._executor_lock:
self._cached_tool_executor = ToolExecutor(new_toolsets)
executor = ToolExecutor(new_toolsets)
preload_oauth_tokens()
eager_load_oauth_tools(executor)
self._cached_tool_executor = executor
self._cached_executor_key = cache_key

return [(name, old.value, new.value) for name, old, new in changes]
Expand Down
17 changes: 17 additions & 0 deletions holmes/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ class ToolApprovalDecision(BaseModel):
approved: bool
save_prefixes: Optional[List[str]] = None # Prefixes to remember for session
feedback: Optional[str] = None # User feedback when denying a tool call
decision: Optional[Dict[str, Any]] = None # Structured decision data (e.g. OAuth callback)


class OAuthCallbackRequest(BaseModel):
toolset_name: str
code: str
code_verifier: Optional[str] = None # Optional: frontend provides when it generated PKCE, Holmes provides when it generated PKCE
redirect_uri: str
client_id: Optional[str] = None
client_secret: Optional[str] = None # Required by some IdPs (e.g. Supabase) that don't support public clients
user_id: Optional[str] = None


class OAuthCallbackResponse(BaseModel):
success: bool
error: Optional[str] = None


class FrontendToolMode(str, Enum):
Expand Down Expand Up @@ -195,6 +211,7 @@ class ChatRequestBaseModel(BaseModel):
trace_span: Optional[Any] = (
None # Optional span for tracing and heartbeat callbacks
)
user_id: Optional[str] = None # User ID from relay session token validation
Comment thread
Avi-Robusta marked this conversation as resolved.

# In our setup with litellm, the first message in conversation_history
# should follow the structure [{"role": "system", "content": ...}],
Expand Down
Loading
Loading