mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(mcp): stop reporting false OAuth success when no token was obtained (#34807)
* docs(code-execution): document HERMES_* env narrowing + passthrough workaround
The execute_code sandbox-child env scrub (108397726, #27303) deliberately
dropped the broad HERMES_ prefix passthrough, keeping only an operational
4-var allowlist (HERMES_HOME/PROFILE/CONFIG/ENV). A script that relied on a
non-secret HERMES_* var (HERMES_BASE_URL, HERMES_KANBAN_DB, HERMES_*_WEBHOOK,
or a plugin-defined one) now sees it unset in the child.
Document the behavior change and the two recovery routes (terminal.env_passthrough
in config.yaml, or required_environment_variables in skill frontmatter), plus
the debug log line that surfaces the drop for diagnosis.
* fix(mcp): stop reporting false OAuth success when no token was obtained
`hermes mcp login` reported "Authenticated — N tool(s) available" for
servers that serve tools/list without auth (e.g. Google's official Drive
MCP server) even when the OAuth flow never completed — dynamic client
registration 400'd because the provider doesn't support RFC 7591, so no
token was ever acquired. Every real tool call then hung until timeout
with no indication of why.
Login now verifies a token actually landed on disk after the probe. When
it didn't, it warns that authentication didn't complete and shows the
config needed to supply a pre-registered client_id/client_secret (the
existing, already-supported workaround for DCR-less providers).
Adds a docs pitfall for Google Drive / Atlassian-style providers.
Fixes #34775
This commit is contained in:
parent
1cb850b674
commit
27a2c4f36f
3 changed files with 115 additions and 0 deletions
|
|
@ -205,6 +205,22 @@ def _probe_single_server(
|
|||
return tools_found
|
||||
|
||||
|
||||
def _oauth_tokens_present(name: str) -> bool:
|
||||
"""Return True if an OAuth token file exists on disk for ``name``.
|
||||
|
||||
Used after ``hermes mcp login`` to distinguish a genuine authentication
|
||||
from a probe that succeeded only because the server allowed
|
||||
initialize/tools-list without auth (so no token was ever acquired).
|
||||
"""
|
||||
try:
|
||||
from tools.mcp_oauth import HermesTokenStorage
|
||||
return HermesTokenStorage(name).has_cached_tokens()
|
||||
except Exception as exc: # pragma: no cover — defensive
|
||||
logger.debug("Could not check OAuth tokens for '%s': %s", name, exc)
|
||||
# Be permissive on unexpected errors: don't block a real success.
|
||||
return True
|
||||
|
||||
|
||||
def _unwrap_exception_group(exc: BaseException) -> Exception:
|
||||
"""Extract the root-cause exception from anyio TaskGroup wrappers.
|
||||
|
||||
|
|
@ -631,6 +647,36 @@ def cmd_mcp_login(args):
|
|||
# Probe triggers the OAuth flow (browser redirect + callback capture).
|
||||
try:
|
||||
tools = _probe_single_server(name, server_config)
|
||||
# A clean probe is NOT proof of authentication. Some MCP servers
|
||||
# (notably Google's official Drive server) serve initialize +
|
||||
# tools/list WITHOUT auth, so the probe lists tools even when the
|
||||
# OAuth flow never completed — e.g. dynamic client registration
|
||||
# 400'd because the provider doesn't support RFC 7591. Reporting
|
||||
# "Authenticated — N tools" in that case is a false success: every
|
||||
# real tool call later hangs until timeout because there's no token.
|
||||
# Verify a token actually landed on disk before claiming success.
|
||||
if not _oauth_tokens_present(name):
|
||||
_warning(
|
||||
"Server responded, but no OAuth token was obtained — "
|
||||
"authentication did not complete."
|
||||
)
|
||||
print()
|
||||
_info(
|
||||
"Some providers (e.g. Google Drive, Atlassian) do not support "
|
||||
"automatic client registration. For those you must create an "
|
||||
"OAuth client yourself and add its credentials to config.yaml:"
|
||||
)
|
||||
print()
|
||||
print(color(f" mcp_servers:", Colors.DIM))
|
||||
print(color(f" {name}:", Colors.DIM))
|
||||
print(color(f" url: {url}", Colors.DIM))
|
||||
print(color(f" auth: oauth", Colors.DIM))
|
||||
print(color(f" oauth:", Colors.DIM))
|
||||
print(color(f" client_id: \"<your-oauth-client-id>\"", Colors.DIM))
|
||||
print(color(f" client_secret: \"<your-oauth-client-secret>\"", Colors.DIM))
|
||||
print()
|
||||
_info("Then re-run `hermes mcp login " + name + "`.")
|
||||
return
|
||||
if tools:
|
||||
_success(f"Authenticated — {len(tools)} tool(s) available")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -595,3 +595,58 @@ class TestMcpLogin:
|
|||
out = capsys.readouterr().out
|
||||
assert "no URL" in out or "not an OAuth" in out
|
||||
|
||||
def test_login_false_success_no_token(self, tmp_path, capsys, monkeypatch):
|
||||
"""Probe lists tools without auth (Google Drive), but no token landed.
|
||||
|
||||
The server allows tools/list without auth (DCR 400'd), so the probe
|
||||
succeeds yet no OAuth token exists. Login must NOT claim success — it
|
||||
should warn and point the user at pre-registered client_id config.
|
||||
"""
|
||||
_seed_config(tmp_path, {
|
||||
"googledrive": {
|
||||
"url": "https://drivemcp.googleapis.com/mcp/v1",
|
||||
"auth": "oauth",
|
||||
},
|
||||
})
|
||||
# Probe returns tools even though auth never completed.
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.mcp_config._probe_single_server",
|
||||
lambda name, cfg: [("search_files", "d"), ("read_file_content", "d")],
|
||||
)
|
||||
# No token file is created → _oauth_tokens_present() returns False.
|
||||
from hermes_cli.mcp_config import cmd_mcp_login
|
||||
|
||||
cmd_mcp_login(_make_args(name="googledrive"))
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert "no OAuth token was obtained" in out
|
||||
assert "Authenticated" not in out
|
||||
assert "client_id" in out
|
||||
|
||||
def test_login_genuine_success_with_token(self, tmp_path, capsys, monkeypatch):
|
||||
"""Probe lists tools AND a token exists → report real success."""
|
||||
_seed_config(tmp_path, {
|
||||
"realserver": {"url": "https://mcp.example.com/mcp", "auth": "oauth"},
|
||||
})
|
||||
token_dir = tmp_path / "mcp-tokens"
|
||||
|
||||
# cmd_mcp_login wipes tokens before probing, then the real OAuth flow
|
||||
# writes a fresh token during the probe. Simulate that: the mocked
|
||||
# probe drops a token file, mirroring a successful authorization.
|
||||
def mock_probe(name, cfg):
|
||||
token_dir.mkdir(exist_ok=True)
|
||||
(token_dir / "realserver.json").write_text('{"access_token": "x"}')
|
||||
return [("a", "d"), ("b", "d"), ("c", "d")]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.mcp_config._probe_single_server", mock_probe
|
||||
)
|
||||
|
||||
from hermes_cli.mcp_config import cmd_mcp_login
|
||||
|
||||
cmd_mcp_login(_make_args(name="realserver"))
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert "Authenticated — 3 tool(s) available" in out
|
||||
assert "no OAuth token" not in out
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,20 @@ On first connect, Hermes prints an authorize URL, opens your browser when possib
|
|||
|
||||
See [OAuth over SSH / Remote Hosts](../../guides/oauth-over-ssh.md#mcp-servers) for the full walkthrough, including DCR-less servers (e.g. Slack), pre-registered `client_id`/`client_secret`, scope customization, and re-auth via `hermes mcp login <server>`.
|
||||
|
||||
**Pitfall — providers that don't support automatic registration (Google Drive, Atlassian).** Some servers reject the dynamic client registration step (RFC 7591) that bare `auth: oauth` relies on — Google's official Drive server (`https://drivemcp.googleapis.com/mcp/v1`) returns a `400 Bad Request`, so no OAuth client is created and no token is acquired. The symptom is subtle: these servers also serve `tools/list` *without* auth, so `hermes mcp login` can list the tools and look like it worked, but every real tool call later times out. `hermes mcp login` now detects this (it checks that a token actually landed on disk) and tells you to supply your own OAuth client. Create one in the provider's console and add it to config:
|
||||
|
||||
```yaml
|
||||
mcp_servers:
|
||||
googledrive:
|
||||
url: "https://drivemcp.googleapis.com/mcp/v1"
|
||||
auth: oauth
|
||||
oauth:
|
||||
client_id: "<your-oauth-client-id>"
|
||||
client_secret: "<your-oauth-client-secret>"
|
||||
```
|
||||
|
||||
Then run `hermes mcp login googledrive` — with the pre-registered client, Hermes skips registration and runs the normal browser authorization flow.
|
||||
|
||||
**Pitfall — config auto-reload race.** When you edit `~/.hermes/config.yaml` from inside a running Hermes session, the CLI auto-reloads MCP connections with a 30s timeout. That's not enough for an interactive OAuth flow. Add the entry, then run `hermes mcp login <server>` from a fresh terminal — it waits the full 5 minutes for you to complete auth.
|
||||
|
||||
## Basic configuration reference
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue