Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,19 @@ Default integration paths for this repo:
- Configure OAuth credentials in `config/google_credentials/`
- If tools fail but show β€œConnected,” check logs under `logs/`

**Rube/Composio (optional) usage:**
**Tool Selection Priority (CRITICAL):**
When handling requests, ALWAYS check for local MCP tools FIRST:
1. **Gmail**: Use `mcp__gmail__*` tools (NOT Rube's GMAIL_* tools)
2. **Calendar**: Use `mcp__google-calendar__*` tools (NOT Rube's GOOGLECALENDAR_* tools)
3. **Reminders**: Use `mcp__reminders-life-planner__*` tools
4. **iMessage**: Use `/wolfies-imessage` skill (Gateway CLI)

**Only use Rube/Composio when:**
- Local MCP is not available for the service (e.g., Slack, Twitter writes)
- Local MCP explicitly fails and you've confirmed it's not a transient error
- User explicitly requests Rube integration

**Rube/Composio usage (fallback only):**
1. `RUBE_SEARCH_TOOLS` to discover tools
2. `RUBE_MULTI_EXECUTE_TOOL` to execute requests

Expand Down
165 changes: 165 additions & 0 deletions Plans/Rust_MCP_Clients_Handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Rust MCP Client Refactor - Handoff Prompt

**For:** Fresh Claude agent
**Project:** LIFE-PLANNER
**Date:** 01/08/2026

---

## Context

We successfully built a high-performance iMessage CLI using the **Rust client + Python daemon** pattern, achieving **19x faster performance** than the original MCP-based approach (40ms vs 763ms per operation).

The architecture:
- **Rust CLI** (`wolfies-imessage`): Fast ~3ms spawn, sends requests via Unix socket
- **Python daemon** (`imessage_daemon.py`): Warm process handling iMessage operations
- **Protocol**: JSON-RPC over Unix socket at `~/.wolfies-imessage/daemon.sock`

This is now distributed via Homebrew (`brew install wolfiesch/executive-function/wolfies-imessage`).

---

## Task: Extend Pattern to Other MCP Servers

Three Python MCP servers currently exist that could benefit from Rust CLI wrappers:

| Module | Current Location | MCP Tools |
|--------|-----------------|-----------|
| **Gmail** | `src/integrations/gmail/server.py` | list_emails, get_email, search_emails, send_email, get_unread_count |
| **Calendar** | `src/integrations/google_calendar/server.py` | list_events, get_event, create_event, find_free_time |
| **Reminders** | `Reminders/mcp_server/server.py` | create_reminder, list_reminders, complete_reminder, delete_reminder, list_reminder_lists |

---

## Reference Implementation

Study these files to understand the pattern:

### Rust Client (shared library + iMessage binary)
```
Texting/gateway/wolfies-client/
β”œβ”€β”€ Cargo.toml # Workspace definition
β”œβ”€β”€ crates/
β”‚ β”œβ”€β”€ wolfies-core/ # Shared library
β”‚ β”‚ β”œβ”€β”€ src/lib.rs # Protocol, client, response types
β”‚ β”‚ └── Cargo.toml
β”‚ └── wolfies-imessage/ # iMessage-specific binary
β”‚ β”œβ”€β”€ src/main.rs # CLI entry point
β”‚ └── Cargo.toml
```

### Python Daemon
```
Texting/gateway/
β”œβ”€β”€ imessage_daemon.py # Unix socket server, handles JSON-RPC
β”œβ”€β”€ imessage_daemon_client.py # Python client for testing
└── output_utils.py # Shared output formatting
```

### Key Files to Read
1. `Texting/gateway/wolfies-client/crates/wolfies-core/src/lib.rs` - Protocol definition
2. `Texting/gateway/wolfies-client/crates/wolfies-imessage/src/main.rs` - CLI structure
3. `Texting/gateway/imessage_daemon.py` - Daemon architecture
4. `Plans/Rust_Gateway_Framework_2026-01-08.md` - Original implementation plan

---

## Benchmarking Approach

We used a normalized workload benchmark to compare implementations:

### Benchmark Script
`Texting/benchmarks/normalized_workload_benchmarks.py`

### Workloads Tested
1. **recent** - Get recent messages (light read)
2. **unread** - Get unread messages (light read)
3. **find** - Search messages (medium read)
4. **send** - Send message (write operation)
5. **contacts** - List contacts (metadata read)

### Metrics Captured
- Cold start time (first request)
- Warm request time (subsequent requests)
- P50, P95, P99 latencies
- Memory usage
- Throughput (requests/second)

### Before/After Comparison
Create baseline benchmarks BEFORE implementing Rust clients, then compare after.

---

## Proposed Phases

### Phase 1: Gmail Rust Client
1. Create `wolfies-gmail` crate in workspace
2. Convert `gmail/server.py` to daemon mode (Unix socket)
3. Implement Rust CLI with subcommands: `list`, `search`, `read`, `send`, `unread`
4. Benchmark before/after
5. Update Homebrew formula

### Phase 2: Calendar Rust Client
1. Create `wolfies-calendar` crate
2. Convert `google_calendar/server.py` to daemon mode
3. Implement Rust CLI: `list`, `get`, `create`, `free-time`
4. Benchmark before/after
5. Update Homebrew formula

### Phase 3: Reminders Rust Client
1. Create `wolfies-reminders` crate
2. Convert `Reminders/mcp_server/server.py` to daemon mode
3. Implement Rust CLI: `create`, `list`, `complete`, `delete`, `lists`
4. Benchmark before/after
5. Update Homebrew formula

---

## Questions to Consider

1. **Is Rust worth it for these?** iMessage benefited because it's used for quick lookups (`unread`, `recent`). Gmail/Calendar/Reminders are primarily MCP servers for Claude Code - direct CLI use is less common. Benchmark first to see if there's a real bottleneck.

2. **Daemon vs Direct?** iMessage uses a daemon because the database reads are expensive to initialize. Gmail/Calendar use OAuth which has its own warm-up cost. Profile to see where time is spent.

3. **Shared credentials?** Gmail and Calendar could share OAuth tokens. Consider a unified Google daemon.

---

## Quick Start Commands

```bash
# Read the iMessage implementation
cat Plans/Rust_Gateway_Framework_2026-01-08.md

# Check the Rust workspace structure
ls -la Texting/gateway/wolfies-client/crates/

# Read the core library
cat Texting/gateway/wolfies-client/crates/wolfies-core/src/lib.rs

# Run existing benchmarks
python3 -m Texting.benchmarks.normalized_workload_benchmarks --help

# Check current MCP servers
cat src/integrations/gmail/server.py | head -100
cat src/integrations/google_calendar/server.py | head -100
cat Reminders/mcp_server/server.py | head -100
```

---

## Success Criteria

- [ ] Baseline benchmarks for each MCP server
- [ ] Rust CLI achieving <50ms for common operations
- [ ] Homebrew formulas updated with Rust binaries
- [ ] Before/after performance comparison documented
- [ ] All existing functionality preserved

---

## Notes

- The Homebrew tap is at `wolfiesch/homebrew-executive-function`
- Release workflow at `.github/workflows/release.yml` handles multi-module builds
- Current version is v0.2.0
78 changes: 59 additions & 19 deletions Reminders/mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,52 @@

Usage:
python mcp_server/server.py

CHANGELOG (recent first, max 5 entries):
01/08/2026 - Added timing instrumentation for performance profiling (Claude)
"""

import sys
import json
import logging
import time
from pathlib import Path
from typing import Optional
from typing import Optional, Any

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))


# =============================================================================
# TIMING INSTRUMENTATION (for profiling)
# =============================================================================

class TimingContext:
"""
Context manager that logs timing to stderr for benchmark capture.

Timing markers are in format: [TIMING] phase_name=XX.XXms
These are parsed by the benchmark runner to capture server-side timing.
"""

def __init__(self, phase_name: str):
self.phase = phase_name
self.start: float = 0

def __enter__(self) -> "TimingContext":
self.start = time.perf_counter()
return self

def __exit__(self, *args: Any) -> None:
elapsed_ms = (time.perf_counter() - self.start) * 1000
print(f"[TIMING] {self.phase}={elapsed_ms:.2f}ms", file=sys.stderr)


def _timing(phase: str) -> TimingContext:
"""Convenience function to create a timing context."""
return TimingContext(phase)
Comment on lines +31 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TimingContext class is a useful utility for performance profiling. Since the PR description mentions adding timing instrumentation to other integrations like Gmail and Calendar, this context manager is likely to be needed in other server files as well.

To promote code reuse and avoid duplication, consider moving TimingContext and its _timing helper function to a shared utility module within the project, for example, in a mcp.utils or mcp.profiling module. This will make it easier to apply consistent timing instrumentation across all MCP servers.



from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
Expand Down Expand Up @@ -267,15 +302,16 @@ async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent
)]

# Create reminder with validation
result = reminder_manager.create_reminder(
title=title,
list_name=arguments.get("list_name"),
due_date=arguments.get("due_date"),
notes=arguments.get("notes"),
priority=validated_priority,
tags=validated_tags,
recurrence=validated_recurrence
)
with _timing("api_create_reminder"):
result = reminder_manager.create_reminder(
title=title,
list_name=arguments.get("list_name"),
due_date=arguments.get("due_date"),
notes=arguments.get("notes"),
priority=validated_priority,
tags=validated_tags,
recurrence=validated_recurrence
)

return [types.TextContent(
type="text",
Expand All @@ -297,12 +333,13 @@ async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent
)]

# List reminders
reminders = reminders_interface.list_reminders(
list_name=arguments.get("list_name"),
completed=arguments.get("completed", False),
limit=limit,
tag_filter=arguments.get("tag_filter")
)
with _timing("api_list_reminders"):
reminders = reminders_interface.list_reminders(
list_name=arguments.get("list_name"),
completed=arguments.get("completed", False),
limit=limit,
tag_filter=arguments.get("tag_filter")
)

return [types.TextContent(
type="text",
Expand All @@ -324,7 +361,8 @@ async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent
)]

# Complete reminder
result = reminder_manager.complete_reminder(reminder_id)
with _timing("api_complete_reminder"):
result = reminder_manager.complete_reminder(reminder_id)

return [types.TextContent(
type="text",
Expand All @@ -333,7 +371,8 @@ async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent

elif name == "list_reminder_lists":
# List all available reminder lists
lists = reminders_interface.list_reminder_lists()
with _timing("api_list_lists"):
lists = reminders_interface.list_reminder_lists()

return [types.TextContent(
type="text",
Expand All @@ -355,7 +394,8 @@ async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent
)]

# Delete reminder
result = reminder_manager.delete_reminder(reminder_id)
with _timing("api_delete_reminder"):
result = reminder_manager.delete_reminder(reminder_id)

return [types.TextContent(
type="text",
Expand Down
8 changes: 6 additions & 2 deletions Reminders/src/reminders_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
Hybrid approach:
- AppleScript for creating reminders (simple, reliable)
- EventKit for reading, completing, deleting (robust querying)

CHANGELOG (recent first, max 5 entries):
01/09/2026 - Increased timeouts from 10s to 15s to reduce intermittent failures (Claude)
"""

import subprocess
Expand Down Expand Up @@ -309,7 +312,7 @@ def create_reminder(
['osascript', '-e', script],
capture_output=True,
text=True,
timeout=10
timeout=15 # Increased from 10s to handle Reminders.app latency
)

if result.returncode == 0:
Expand Down Expand Up @@ -579,7 +582,8 @@ def fetch_callback(reminders):
)

# Wait for callback to complete (with timeout)
fetch_complete.wait(timeout=10.0)
# Increased from 10s to 15s to handle EventKit async latency
fetch_complete.wait(timeout=15.0)

if not fetch_complete.is_set():
logger.error("EventKit fetch timed out")
Expand Down
Loading
Loading