This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-D-DAEMON ST2 — web + MCP + heartbeat listeners#101
Merged
Conversation
Three concrete listeners plug into the ST1 coordinator. After ST2,
`kora daemon` boots a complete admin-UI + MCP + scheduler runtime
on the internal port 9119.
## New modules
- `kora_cli/listeners/__init__.py` — explicit-order package init.
Importing the package triggers heartbeat → web → mcp registrations.
- `kora_cli/listeners/heartbeat.py` — pure-asyncio `HeartbeatScheduler`
with `register_periodic_task(name, interval, callable)` API for
Feature 2 to consume. Pre-registers `kora.daemon.alive` placeholder
task (30s no-op log). Failures in one task don't kill the loop;
shutdown cancels all + awaits in-flight callbacks.
- `kora_cli/listeners/web.py` — `WebListener` wraps existing
`kora_cli.web_server.app` via programmatic uvicorn (Server.serve
+ should_exit). Binds 127.0.0.1:9119 by default; `KORA_WEB_HOST`
/ `KORA_WEB_PORT` env overrides for tests + local dev.
- `kora_cli/listeners/mcp.py` — NEW MCP HTTP server mounted at `/mcp`
on the shared admin app. MINIMAL surface per spec: only
`kora__daemon_status` returning coordinator state + listener health
+ uptime via `DaemonCoordinator.get_status()`. Bearer auth from
`KORA_MCP_BEARER_TOKEN`, fail-CLOSED on unset/empty. Two surfaces:
GET `/mcp/tools/list` (bucket-spec smoke test path) + POST `/mcp`
(JSON-RPC 2.0 for full MCP client compat — handles `tools/list` +
`tools/call`). Constant-time bearer comparison via `hmac.compare_digest`.
## daemon.py additions (+82 lines)
- `DaemonCoordinator.get_status()` — JSON-serializable snapshot
consumed by the MCP `kora__daemon_status` tool. Surfaces state
(`booting`/`running`/`shutting_down`), uptime, shutdown reason,
per-listener registered+started state.
- Module-level `current_coordinator()` accessor — set by `cmd_daemon`
during run; tests instantiate coordinators directly without
polluting global state.
- `cmd_daemon` imports `kora_cli.listeners` (lazy) so listener
modules' import-time `register_daemon_listener` calls fire.
## Route-mount ordering (caught + fixed)
`kora_cli/web_server.py:6412` runs `mount_spa(app)` at module-import
time, which adds a `/{full_path:path}` SPA catch-all. Plain
`app.include_router(router)` adds AFTER it — catch-all wins on
`/mcp/*`. Fixed by inserting MCP routes at the FRONT of `app.routes`
via a scratch sub-app to resolve dependency wiring first.
## Tests (29 new, all passing — 44 total with ST1)
- `test_heartbeat.py` — 8 tests: register validation, overwrite
semantics, pre-registered alive task, multi-fire periodic,
cancellation on shutdown, failing-task isolation, empty scheduler,
first-fire-delayed-by-interval (no thundering herd).
- `test_web.py` — 5 tests: full lifecycle with httpx GET, double-
shutdown safe, env overrides, non-int port rejection, defaults.
- `test_mcp.py` — 16 tests: GET /mcp/tools/list valid/no-bearer/
wrong-bearer/malformed/env-unset, POST tools/list, POST tools/call
daemon_status, unknown method (-32601), unknown tool (-32602),
parse error (-32700), MCPListener.startup fail-CLOSED on unset env,
startup fail-CLOSED on whitespace env, startup ok with env,
status with no coordinator, status with active coordinator, tool
descriptor schema.
End-to-end smoke (KORA_DEV=1 KORA_MCP_BEARER_TOKEN=tok kora daemon):
all 3 listeners boot, `/mcp/tools/list` returns the tool, `/mcp` POST
tools/call returns live coordinator state JSON showing
`listeners: [heartbeat:started, web:started, mcp:started]`, SIGTERM
exits 0.
## §6 ship checklist
- [x] PR base `feature/phase2-upgrades`
- [x] PR title `feat(kora): KR-D-DAEMON ST2 — web + MCP + heartbeat listeners`
- [x] Tests pass locally (44/44)
- [x] No new dependencies (FastAPI + uvicorn already in `web` extra)
- [x] K-DG: no further drift surfaced; ST1 corrections still hold
## What's next
ST3: webhook routers (Slack events + email inbound) on the SEPARATE
public-port FastAPI app (9118 per PM Q1 ruling); R2 §5 amendment doc.
Then KR-D-DEPLOY follow-on bucket.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
Three concrete listeners plug into the ST1 coordinator. After this PR, `kora daemon` boots a complete admin-UI + MCP + scheduler runtime on the internal port 9119.
Bucket spec: `kora_docs/17_cc_bucket_prompts/KR-D-DAEMON_always_alive_runtime.md` (commit 54032c6).
Base: `feature/phase2-upgrades` — NOT main.
New modules
daemon.py additions (+82 lines)
Route-mount ordering — caught + fixed
`kora_cli/web_server.py:6412` runs `mount_spa(app)` at module-import time, adding a `/{full_path:path}` SPA catch-all. Plain `app.include_router(router)` adds AFTER it — catch-all wins on `/mcp/*`. Fixed by inserting MCP routes at the FRONT of `app.routes` via a scratch sub-app to resolve dependency wiring first.
Tests (29 new, 44 total all passing)
End-to-end smoke (live daemon)
```
KORA_DEV=1 KORA_MCP_BEARER_TOKEN=tok KORA_WEB_PORT=9189 kora daemon
```
§6 ship checklist
What's next
🤖 Generated with Claude Code