Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-MCP-SEND-TOOLS — expose Slack DM + email send via /mcp#130

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-MCP-SEND-TOOLS
May 23, 2026
Merged

feat(kora): KR-MCP-SEND-TOOLS — expose Slack DM + email send via /mcp#130
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-MCP-SEND-TOOLS

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Bridges CC#3's SlackClient (#122) + CC#1's PurelymailClient (#124)
onto the agent-facing `/mcp` surface. Other agents can now drive
Kora-initiated sends through 2 new mutating tools.

Bucket spec: `17_cc_bucket_prompts/KR-MCP-SEND-TOOLS_agent_driven_sends.md`

New tools

Tool Args Cap Notes
`kora__send_slack_dm` channel_id, text (≤4000), thread_ts? `kora__send_slack_dm` D-prefix OR KORA_SLACK_JOSHUA_USER_ID match; bot identity = Kora
`kora__send_email` to (≤10), subject, body_text, body_html?, in_reply_to? `kora__send_email` from_addr derived from env (NOT caller-controllable); NO attachments

Both `requires_cap_gate=True` → -32001 `capability_denied` if
caller's `allowed_caps` doesn't include the cap name.

§5 PM rulings applied

Q Resolution
Q1 listener-promotion compat Slack handler's `_get_or_create_slack_client` updated to check listener accessor FIRST + fall back to lazy-construct. Existing tests pass without changes.
Q2 caller_actor_kind placement Single JSONL per channel + new optional `caller_actor_kind` field (omitted on handler-driven sends; populated on MCP-driven). Backwards-compat for consumers.
Q3 Slack channel_id validation D-prefix OR KORA_SLACK_JOSHUA_USER_ID match. U-prefix + C-prefix rejected at MCP layer.

Surface changes

Layer Net
`kora_cli/listeners/slack_client_listener.py` (NEW) 142 LOC — fail-soft listener + `current_slack_client()` accessor
`kora_cli/listeners/purelymail_client_listener.py` (NEW) 138 LOC — same shape; fail-soft on missing SMTP envs
`kora_cli/listeners/mcp_tools.py` +323 — 2 new tool descriptors + dispatchers + Pydantic results
`kora_cli/listeners/init.py` +8 — wire-in for both new listeners
`kora_cli/handlers/slack_dm_handler.py` +69/-25 — `_get_or_create_slack_client` checks listener accessor first; outbound JSONL entry gains optional `caller_actor_kind`
`kora_cli/clients/purelymail_client.py` +20/-5 — `send_email` + `send_email_internal` gain `caller_actor_kind` kwarg; threaded through to JSONL log

Security carry-forward

  • SlackClient bot token never in error envelope or JSONL (exception text replaced by type-name on re-raise)
  • PurelymailClient SMTP password never in error envelope or JSONL (same + existing client `_sanitize_error`)
  • Diverse-failure tests pin both invariants

Test plan

  • 36 new tests pass (11 listener lifecycle + 25 MCP dispatcher)
  • 341/341 cross-bucket regression (test_listeners/ +
    test_heartbeat_probes/ + test_clients/ + test_handlers/)
  • Ruff clean
  • Slack handler's lazy-construct fallback still works (verified
    via existing handler tests in regression suite)

Cascade

Base: `feature/phase2-upgrades`. Single PR.

After merge

Other agents (other PMs, drone tickets, future Kora-driven multi-
agent workflows) can drive Slack DMs + emails through Kora's MCP
surface. Tool inventory: 8 read + 5 mutating = 13 tools on the
agent-facing surface. Attachment support in `kora__send_email`
remains the open follow-on (file-upload semantics design needed).

🤖 Generated with Claude Code

@rafe-walker rafe-walker force-pushed the feat/kora-KR-MCP-SEND-TOOLS branch from 4d42011 to ccaae9e Compare May 23, 2026 00:27
Bridges CC#3's SlackClient (#122) + CC#1's PurelymailClient (#124)
onto the agent-facing /mcp surface. Other agents (other PMs, drone
tickets, future Kora-driven multi-agent workflows) can now drive
Kora-initiated sends.

`kora__send_slack_dm`:
  - Args: channel_id (D-prefix or KORA_SLACK_JOSHUA_USER_ID),
    text (≤4000 chars), thread_ts (optional)
  - requires_cap_gate=True (-32001 capability_denied if absent)
  - Channel-broadcast defense: U-prefix + C-prefix rejected at
    MCP layer (operator pre-resolves to DM channel)
  - SlackClient-unavailable surface: -32001 with
    "slack_client_unavailable" prefix (KORA_SLACK_BOT_TOKEN unset
    or daemon not running with slack_client listener)
  - Bot identity = Kora (NOT caller-controllable)

`kora__send_email`:
  - Args: to (≤10), subject, body_text, body_html (optional),
    in_reply_to (optional)
  - requires_cap_gate=True
  - from_addr derived from KORA_PUREMAIL_SMTP_USERNAME — NOT
    caller-controllable (prevents sender impersonation)
  - NO attachments in this bucket (deferred to
    KR-MCP-SEND-TOOLS-ATTACHMENTS; file-upload semantics
    aren't trivially MCP-compatible)
  - PurelymailClient-unavailable surface: -32001 with
    "purelymail_client_unavailable" prefix

Both tools added to ST2_TOOL_DESCRIPTORS + ST2_TOOL_DISPATCH;
schema validation surfaces -32602 invalid_params for malformed
inputs before any client invocation.

`kora_cli/listeners/slack_client_listener.py` (NEW):
  - SlackClientListener: startup constructs SlackClient if
    KORA_SLACK_BOT_TOKEN set; FAIL-SOFT if unset (daemon boot
    continues; outbound disabled with INFO log)
  - current_slack_client() module-level accessor mirroring the
    current_pool() / current_coordinator() pattern

`kora_cli/listeners/purelymail_client_listener.py` (NEW):
  - PurelymailClientListener: startup constructs PurelymailClient
    if SMTP envs set; FAIL-SOFT if missing
  - current_purelymail_client() accessor

Both registered at module-import time via the listeners package
__init__; both fail-soft so outbound capabilities never block
daemon boot.

`kora_cli/handlers/slack_dm_handler.py:_get_or_create_slack_client`
updated:
  - Order: cached client → listener accessor → lazy-construct
  - Lazy-construct fallback PRESERVED so standalone-handler tests
    (no daemon listeners) keep passing
  - Production daemon paths get the shared listener instance
  - Existing reasoning-engine reply path (CC#3 KR-FEAT-AI-RESPONSE-
    LOOP) goes through this helper → automatically picks up the
    listener instance once daemon's running

`caller_actor_kind` field added to outbound entries in BOTH
log files:

  - slack_dm_log.jsonl: handler's
    `_append_outbound_log_entry` gains optional caller_actor_kind
    kwarg (omitted when None to preserve back-compat with
    consumers expecting absence on handler-driven sends)
  - email_outbound_log.jsonl: PurelymailClient's send_email gains
    caller_actor_kind kwarg, propagated to `_append_outbound_log`

MCP tool dispatchers pass caller.actor_kind through; handler /
runtime-driven sends omit the field (None).

Tool 1 rejects channel_ids that don't start with `D` OR match
KORA_SLACK_JOSHUA_USER_ID. U... user-IDs are rejected at MCP
layer (would require an extra Slack API call to resolve to DM
channel; operator/agent must pre-resolve). C... public/private
channels also rejected — channel broadcast is a separate threat
model + would need its own bucket.

  - SlackClient bot token never in error envelope or JSONL
    (sanitized via type-name-only re-raise on exception)
  - PurelymailClient SMTP password never in error envelope or
    JSONL (sanitized via same pattern + existing PurelymailClient
    _sanitize_error)
  - Diverse-failure-mode tests pin both invariants

36 new tests across 2 files:

`tests/kora_cli/test_listeners/test_send_client_listeners.py` (11):
  - Both listeners registered at import time
  - Slack listener fail-soft on missing token + WARN log
  - Slack listener constructs cleanly when token set
  - PurelymailClient listener fail-soft on missing username OR password
  - PurelymailClient listener constructs with full env
  - Both listeners clear singleton on shutdown
  - Both listeners handle unexpected construction errors

`tests/kora_cli/test_listeners/test_mcp_send_tools.py` (25):
  - 6 registry-side wiring tests (descriptors present,
    requires_cap_gate=True, dispatch registered, schema caps)
  - 5 slack input validation tests (empty channel/text;
    >4000 chars; non-DM channel rejected; Joshua's user ID OK)
  - 1 SlackClient unavailable
  - 2 slack happy-path tests (SendResult shape + JSONL entry
    with caller_actor_kind)
  - 1 slack token-absence-in-error test
  - 4 email input validation tests
  - 2 email client-unavailable tests (listener None + env unset)
  - 1 email happy-path test (from_addr from env;
    caller_actor_kind threaded)
  - 1 email password-absence-in-error test
  - 1 caller_actor_kind end-to-end propagation test

36/36 pass; 341/341 cross-bucket regression
(test_listeners/ + test_heartbeat_probes/ + test_clients/ +
test_handlers/). Ruff clean.

Agent-facing /mcp surface gains 2 mutating tools (total now: 5
mutating + 8 read = 13 tools). Other agents have a complete
operational surface on Kora — read state, request transitions,
create Sea_Tickets, send Slack DMs, send emails. The remaining
follow-on (attachment support in send_email) ships when the
file-upload semantics get a design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker force-pushed the feat/kora-KR-MCP-SEND-TOOLS branch from ccaae9e to 8d05ae5 Compare May 23, 2026 00:37
@rafe-walker rafe-walker merged commit c3decdd into feature/phase2-upgrades May 23, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-MCP-SEND-TOOLS branch May 23, 2026 00:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant