docs(integrations): document the defensive getattr in the Hermes provider

Addresses the remaining review finding (deferred while #859 was open; now merged,
so the hermes module is on main and the comment lands cleanly here). The two
getattr sites read MCP SDK result/tool fields; `mcp` is an unpinned dependency
(plugin.yaml), and its CallToolResult / ListToolsResult / Tool shapes have varied
across SDK versions — so the defensive getattr is intentional, not the speculative
kind CLAUDE.md warns against. Added constraint comments at both sites explaining why.

No behavior change. `just package-check-hermes` passes (244 passed, 12 integration
tests skipped without BM_INTEGRATION).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-05-31 11:14:39 -05:00
committed by Paul Hernandez
parent 63f5c62dfd
commit bfee8958ad
+6
View File
@@ -449,6 +449,9 @@ def _extract_mcp_text(result: Any) -> str:
Returns a JSON string for the agent. If the result is itself JSON, returns it
as-is. Otherwise wraps the text in `{"text": "..."}` for downstream parsing.
"""
# The `mcp` SDK is an unpinned dependency (see plugin.yaml), and its result
# shapes (CallToolResult, content blocks) have shifted across versions — read
# these fields defensively with getattr rather than direct attribute access.
is_error = bool(getattr(result, "isError", False))
parts: list[str] = []
for c in getattr(result, "content", None) or []:
@@ -594,6 +597,9 @@ class _BmMcpActor:
self._stop_future = asyncio.get_running_loop().create_future()
try:
listing = await session.list_tools()
# Same unpinned-`mcp`-SDK reason as parse_result above:
# ListToolsResult / Tool field shapes vary across SDK versions,
# so read them defensively rather than by direct attribute access.
self._tools_cache = [
{
"name": getattr(t, "name", ""),