feat: claude-code-relay provider + hermes-tools MCP server - #20
Merged
Conversation
NousResearch#109 Minimal stdio JSON-RPC server that responds to MCP 'initialize' with the expected protocol version + capabilities. Tools surface comes in follow-up commits. Env-bound to (profile, project) scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109 Filters the live Hermes tool registry to the v1 surface defined in spec §4.1. Includes special hermes_set_task tool for per-turn task context updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109 hermes_set_task updates per-turn task context (task_id, workspace). All other tools/call requests filter through ALLOWED_TOOLS, then dispatch via registry.dispatch() which handles both sync and async tools. Denied tools (kanban_dispatch etc.) return isError=true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#113 Per spec §4.7 — tracks (profile, project) → tmux session, claude session_id, FIFO path, MCP config path, project root, scope cwd, task count, timestamps. Helpers: upsert_session, get_session, touch_session, mark_compacted, evict_session, list_idle_sessions.
NousResearch#114 derive_project maps a kanban workspace to its 'project' key: - scratch workspaces -> 'scratch' - git workspaces -> basename of git root - else raises ProviderError (B11: no silent default) build_task_header prepends '[task_id=... workspace=...]' to each user prompt so claude knows the current task without an MCP roundtrip.
NousResearch#112 Registers the provider via the existing bundled-plugin pattern under plugins/model-providers/claude-code-relay/__init__.py. api_mode= 'claude_code_relay' is a new tag for the transport defined in the next commit. auth_type='none' because the claude binary handles its own OAuth — the relay just shells out.
NousResearch#112 ScopeContext bundles (profile, project, workspace). ensure_scope either finds a live tmux or invokes relay-spawn-scope.sh (writing the MCP config first). send_turn prepends the task header and relays through relay-send.sh, returning the captured response. Also added monkeypatch for derive_project_root in spawn test since tmp_path is not a git repo.
NousResearch#112 chat_completion() returns the standard OpenAI ChatCompletion object. Only the latest user message is forwarded — prior conversation rides on claude's in-session memory. Usage fields left null; /compact hook in next task captures /usage and backfills.
NousResearch#115 After a task with provider=claude-code-relay completes or blocks, send /compact to its (profile, project) scope. Per B10, failures are logged but never block the completion. Records last_compacted_at on task_sessions. Also: add tasks.provider column to SCHEMA_SQL + additive ALTER migration; set conn.row_factory=sqlite3.Row in init_db test-fixture shortcut so complete_task works correctly in :memory: tests. Spike 2 (2026-06-15) confirmed /compact returns 'Not enough messages to compact' on short sessions; that's handled cleanly via the timeout in relay-send.sh slash-command extension. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#116 evict_idle_scopes deletes task_sessions rows whose last_used_at is older than HERMES_RELAY_IDLE_EVICT_SECS (default 86400 = 24h), and tears down the tmux session via relay-kill-scope.sh. Wire into nightly-hermes-maintenance follows separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109 B3: Stop hook is mandatory for the relay to detect turn-end via FIFO. Boot-check raises ProviderError with installer path if the hook is missing — fail loud instead of silently hanging 180s per turn. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per code review on PR #20 — the shell relay-spawn-scope.sh validates ^[a-zA-Z0-9_-]+$ but Python-side _write_mcp_config writes /tmp/hermes-mcp-{slug}.json BEFORE invoking the shell. Slugs containing / or .. would have allowed path traversal out of /tmp. Add same regex check in ScopeContext.__post_init__. Closes B8 (spec §3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Defense-in-depth: prevent workspace paths in the MCP config from leaking to other users sharing /tmp. Per code review on PR #20. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Transport reads HERMES_RELAY_BIN; the kanban_db /compact hook should too. Per code review on PR #20. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per code review on PR #20 — previously evicted the DB row even on kill failure, orphaning the tmux session with no record to retry. Now check rc; only evict DB row on rc=0. Failed scopes get retried on next cron tick. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per code review on PR #20 — exit 4 means partial response, but callers had no signal. Add a logger.warning so operators see the partial in logs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase 0a + 0b of Milestone 1 (1Team-Engineering/hermes-jarvis#106) — the foundation, MCP server, DB state, provider plugin, transport, lifecycle hooks, and boot checks for the
claude-code-relayprovider.This unlocks Hermes profiles to bill against the Max plan (via interactive
claudeCLI in tmux) instead of API extra-usage, fixing the HTTP 400 wall hit on 2026-06-15 during Phase 2.6 build.Spec:
~/.hermes/plans/claude-code-relay-provider-2026-06-15.md(v3 — per-(profile, project) sessions)Implementation plan:
~/.hermes/plans/claude-code-relay-milestone-1-implementation-plan-2026-06-15.mdWhat this PR ships
hermes_cli/mcp_servers/)claude --mcp-confighermes_cli/kanban_db.py)task_sessionstable + 6 helpers (upsert/get/touch/mark_compacted/evict/list_idle)agent/claude_code_relay_helpers.py)derive_project(git root basename orscratch, no silent default — B11),build_task_headerplugins/model-providers/claude-code-relay/)ProviderProfileregistration withapi_mode='claude_code_relay', auth_type='none' (claude binary handles OAuth)agent/claude_code_relay_transport.py)ScopeContext,ensure_scope(idempotent spawn),send_turn,chat_completionOpenAI-compathermes_cli/kanban_db.py,hermes_cli/relay_eviction.py)/compacton kanban_complete/block (B10 non-blocking), idle-eviction at 24h default_check_stop_hookvalidates~/.claude/settings.jsonhas the Hermes Stop hook (B3 enforcement)Sibling work on
~/.hermesbranchspike/tmux-relay-slack-routing-2026-06-13(4 commits, ships separately):_lib.shencode_cwdrealpath fix (macOS/tmpsymlink bug)relay-spawn-scope.sh— per-(profile, project) tmux+claude spawnerrelay-kill-scope.sh+state_clearhelperrelay-send.shslash-command extension (no Stop-hook wait for/usage,/compact, etc.)Bypass discipline (spec §3) enforced
allowedTools='*'— missing MCP config fails loud[a-zA-Z0-9_-]+validation in spawn script/compactfailures don't block next taskTest plan
pytest tests/test_hermes_tools_mcp.py tests/test_claude_code_relay_helpers.py tests/test_task_sessions_db.py tests/test_relay_compact_hook.py tests/test_relay_eviction.py)hermes-tools-mcpboots and responds toinitializeNotable adaptations from spec
The subagents implementing this PR adapted to the actual codebase shape:
ProviderProfileusesauth_type(notauth_kind),fallback_models(notmodels)get_all_tool_names(),get_entry(),dispatch()(used for the MCPtools/list/tools/callhandlers)plugins/model-providers/claude-code-relay/(the actual discovery path)memory,patch,kanban_listin place ofkanban_log,edit_file,gbrain_memory_*)init_dbextended to acceptsqlite3.Connectionfor in-memory test fixturesSCHEMA_SQLAND additive ALTER add thetasks.providercolumn (fresh in-memory DBs need it without migration)These improvements should be reviewed as part of the merge gate.
Empirical foundation
S0 pre-flight + 2 spikes (all 2026-06-15) established:
claudebills Max plan (rate_limit_event: isUsingOverage=false, overageDisabledReason=org_level_disabled)claude --resume <session-id>preserves multi-turn context across tmux kill+respawn--append-system-prompt-file)/compacthas minimum-turn threshold ("Not enough messages to compact") — handled gracefully via B10Linked issues
🤖 Generated with Claude Code