A signed-in, paid Nous account that the portal had not enabled for connectors got `manage_connections` in its schema and a raw "tool gateway request failed with status 404" back from every call. The gateway answers 404 for any such account by design, and Hermes gated the tool on paid access or a free tool pool, which says nothing about that. The gate now reads the portal's own answer: a `managed_tools` token claim, plus the existing free-tier leg. The gate is also the tool's check_fn, so a session without the claim never sees the tool and the model has no 404 to narrate. A token without the claim reads as not enabled.
126 lines
5.9 KiB
Python
126 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Connection lifecycle tool for managed gateway accounts and local MCP servers.
|
|
|
|
Disconnecting accounts remains a portal-only user decision.
|
|
"""
|
|
|
|
from typing import Any, Callable, Dict, Optional
|
|
|
|
from tools.connectors.gateway import config as gateway_config
|
|
from tools.connectors.managed import run_managed_action
|
|
from tools.connectors.mcp import run_mcp_operation
|
|
from tools.connectors.targets import ALL_ACTIONS, MCP_ACTIONS, normalize_targets, validate_action
|
|
from tools.registry import registry, tool_error
|
|
|
|
|
|
def manage_connections(
|
|
args: Dict[str, Any],
|
|
*,
|
|
client_factory: Optional[Callable[[], Any]] = None,
|
|
session_id: Optional[str] = None,
|
|
tool_call_id: Optional[str] = None,
|
|
connection_callback: Optional[Callable[[Dict[str, Any]], Optional[str]]] = None,
|
|
connectors_available: Optional[Callable[[], bool]] = None,
|
|
) -> str:
|
|
action = str(args.get("action") or "status").strip().lower()
|
|
managed, mcp_targets, target_error = normalize_targets(args.get("connectors"))
|
|
if target_error:
|
|
return tool_error(target_error)
|
|
action_error = validate_action(action, managed, mcp_targets)
|
|
if action_error:
|
|
return tool_error(action_error)
|
|
|
|
if action in MCP_ACTIONS:
|
|
return run_mcp_operation(
|
|
mcp_targets, action,
|
|
connection_callback=connection_callback, session_id=session_id, tool_call_id=tool_call_id,
|
|
)
|
|
|
|
return run_managed_action(
|
|
action, managed, args,
|
|
client_factory=client_factory, session_id=session_id, tool_call_id=tool_call_id,
|
|
connection_callback=connection_callback, connectors_available=connectors_available,
|
|
)
|
|
|
|
|
|
MANAGE_CONNECTIONS_SCHEMA = {
|
|
"name": "manage_connections",
|
|
"description": (
|
|
"Connect the user to apps: managed connector accounts (Gmail, Notion, ...) served "
|
|
"through the tool gateway, and local MCP servers from the catalog. Targets go in "
|
|
"'connectors': a bare slug or {\"name\": \"gmail\"} is a managed connector; "
|
|
"{\"name\": \"linear\", \"mcp\": true} is a local MCP server. "
|
|
"Managed actions: 'status' lists connectors and whether each is connected; 'connect' "
|
|
"starts an authorization for the given connectors; 'reconnect' checks each one and "
|
|
"repairs only what is not connected ('force': true restarts even a working one, for an "
|
|
"account switch). Pass SEVERAL slugs in one call. In the desktop app the call shows the "
|
|
"user a card and blocks until every app is connected, skipped, or the deadline passes; "
|
|
"the result lists each target as connected / skipped / not_connected and never carries "
|
|
"a link. Elsewhere the result carries a connect_url per app for the USER to open in a "
|
|
"browser (never open it yourself); ask them to say when they are done, then use 'status'. "
|
|
"When a connector tool call returns CONNECTION_REQUIRED, use 'connect'. "
|
|
"MCP actions (targets must carry \"mcp\": true): 'install' adds a catalog entry, "
|
|
"'enable' re-enables a disabled configured server, 'authorize' runs its OAuth. "
|
|
"They show the user an approval card and block until it settles. Never hand-edit "
|
|
"mcp_servers config — always use this tool. Never re-ask after a skip or timeout: continue "
|
|
"without the app or ask in chat. A newly installed or authorized server's tools arrive on "
|
|
"your next turn. Off the desktop app the MCP targets come back 'unavailable' with the "
|
|
"terminal commands to give the user. This tool can NOT disconnect, delete, or revoke an "
|
|
"account — that is deliberately user-only. When asked, say so and direct the user to the "
|
|
"Nous Portal (their org's Connectors page) or the desktop app."
|
|
),
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {
|
|
"type": "string",
|
|
"enum": list(ALL_ACTIONS),
|
|
"description": "Defaults to status. install/enable/authorize need mcp:true targets.",
|
|
},
|
|
"connectors": {
|
|
"type": "array",
|
|
"items": {
|
|
"anyOf": [
|
|
{"type": "string"},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"mcp": {"type": "boolean", "description": "true = local MCP server."},
|
|
},
|
|
"required": ["name"],
|
|
"additionalProperties": False,
|
|
},
|
|
]
|
|
},
|
|
"description": (
|
|
"Targets. REQUIRED for every action but status "
|
|
"(e.g. [\"gmail\", {\"name\": \"linear\", \"mcp\": true}]); optional filter for status."
|
|
),
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "reconnect only: restart the authorization even if the app is connected (account switch).",
|
|
},
|
|
},
|
|
"required": [],
|
|
},
|
|
}
|
|
|
|
|
|
registry.register(
|
|
name="manage_connections",
|
|
toolset="connections",
|
|
schema=MANAGE_CONNECTIONS_SCHEMA,
|
|
# The portal gate decides schema presence: an account the portal has not enabled for
|
|
# connectors never sees the tool, so the model cannot call it and read the gateway's
|
|
# 404 back to them. The handler runs the same gate so the RPC path (methods_connectors) and
|
|
# a cached schema agree. Read as a module attribute so tests patch
|
|
# ``gateway.config.connectors_available`` at one seam.
|
|
handler=lambda args, **kw: manage_connections(
|
|
args, session_id=kw.get("session_id"), connectors_available=gateway_config.connectors_available,
|
|
),
|
|
check_fn=lambda: gateway_config.connectors_available(),
|
|
emoji="🔗",
|
|
)
|