claude-handoff-manager 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_handoff_manager-0.1.0/.gitignore +6 -0
- claude_handoff_manager-0.1.0/LICENSE +21 -0
- claude_handoff_manager-0.1.0/PKG-INFO +162 -0
- claude_handoff_manager-0.1.0/README.md +141 -0
- claude_handoff_manager-0.1.0/pyproject.toml +37 -0
- claude_handoff_manager-0.1.0/src/handoff_manager/__init__.py +3 -0
- claude_handoff_manager-0.1.0/src/handoff_manager/server.py +310 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Country Delight Engineering
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-handoff-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that automates Claude Code conversation handoffs — no more manual copy-paste when you hit the message cap.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AashiDutt/claude-handoff-manager
|
|
6
|
+
Author: Country Delight Engineering
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: claude,claude-code,handoff,mcp,model-context-protocol
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# claude-handoff-manager
|
|
23
|
+
|
|
24
|
+
MCP server that automates Claude Code conversation handoffs. No more manual copy-paste when you hit the message cap.
|
|
25
|
+
|
|
26
|
+
## The Problem
|
|
27
|
+
|
|
28
|
+
When a Claude Code conversation reaches the message limit, it produces a handoff summary. You then have to:
|
|
29
|
+
1. Copy the handoff text
|
|
30
|
+
2. Exit / clear the conversation
|
|
31
|
+
3. Start a new session
|
|
32
|
+
4. Paste the handoff as the first message
|
|
33
|
+
|
|
34
|
+
This is tedious and breaks your flow.
|
|
35
|
+
|
|
36
|
+
## The Solution
|
|
37
|
+
|
|
38
|
+
This MCP server automates the entire handoff cycle:
|
|
39
|
+
|
|
40
|
+
1. **End of conversation**: Claude calls `save_handoff` - the message is saved to disk and copied to your clipboard
|
|
41
|
+
2. **New conversation**: Say `"continue from last handoff"` - Claude calls `load_handoff` and picks up exactly where you left off
|
|
42
|
+
|
|
43
|
+
Zero manual copy-paste.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### Option A: `uvx` from Git (recommended for teams)
|
|
48
|
+
|
|
49
|
+
Push this repo to your team's Git host, then each developer adds it to their `~/.mcp.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"handoff-manager": {
|
|
55
|
+
"command": "uvx",
|
|
56
|
+
"args": ["--from", "git+https://github.com/YOUR_ORG/claude-handoff-manager", "claude-handoff-manager"],
|
|
57
|
+
"type": "stdio"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Option B: `uvx` from PyPI
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"handoff-manager": {
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": ["claude-handoff-manager"],
|
|
71
|
+
"type": "stdio"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Option C: Run directly (local development)
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"handoff-manager": {
|
|
83
|
+
"command": "python3",
|
|
84
|
+
"args": ["/path/to/handoff-manager/src/handoff_manager/server.py"],
|
|
85
|
+
"type": "stdio"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
After editing `~/.mcp.json`, **restart Claude Code** to pick up the new server.
|
|
92
|
+
|
|
93
|
+
## Tools
|
|
94
|
+
|
|
95
|
+
| Tool | What it does |
|
|
96
|
+
|------|-------------|
|
|
97
|
+
| `save_handoff` | Saves handoff message to `~/.claude/handoffs/` + copies to clipboard |
|
|
98
|
+
| `load_handoff` | Loads the latest (or a specific) handoff in a new conversation |
|
|
99
|
+
| `copy_handoff_to_clipboard` | Copies a saved handoff to the system clipboard |
|
|
100
|
+
| `list_handoffs` | Lists all saved handoffs with previews |
|
|
101
|
+
| `delete_handoff` | Deletes a specific or all handoffs |
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### Saving (end of a conversation)
|
|
106
|
+
|
|
107
|
+
Claude will automatically call `save_handoff` when it generates a handoff summary at the message cap. You can also manually trigger it:
|
|
108
|
+
|
|
109
|
+
> "Save a handoff with the current context"
|
|
110
|
+
|
|
111
|
+
### Loading (start of a new conversation)
|
|
112
|
+
|
|
113
|
+
> "Continue from last handoff"
|
|
114
|
+
|
|
115
|
+
or
|
|
116
|
+
|
|
117
|
+
> "Load handoff"
|
|
118
|
+
|
|
119
|
+
Claude calls `load_handoff` and gets the full context from your previous session.
|
|
120
|
+
|
|
121
|
+
### Listing saved handoffs
|
|
122
|
+
|
|
123
|
+
> "Show me my saved handoffs"
|
|
124
|
+
|
|
125
|
+
### Clipboard
|
|
126
|
+
|
|
127
|
+
> "Copy the last handoff to my clipboard"
|
|
128
|
+
|
|
129
|
+
## Storage
|
|
130
|
+
|
|
131
|
+
Handoffs are stored as JSON files in `~/.claude/handoffs/`:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
~/.claude/handoffs/
|
|
135
|
+
20260611_143022_home-user-myproject.json
|
|
136
|
+
20260611_100515_home-user-otherproject.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Each file contains:
|
|
140
|
+
- `id`: Unique identifier (timestamp + project slug)
|
|
141
|
+
- `created_at`: UTC timestamp
|
|
142
|
+
- `working_dir`: The project directory
|
|
143
|
+
- `project_name`: Human-friendly name
|
|
144
|
+
- `message`: The full handoff text
|
|
145
|
+
|
|
146
|
+
## Clipboard Support
|
|
147
|
+
|
|
148
|
+
Automatically copies to clipboard on save using:
|
|
149
|
+
- **macOS**: `pbcopy`
|
|
150
|
+
- **Linux X11**: `xclip` or `xsel`
|
|
151
|
+
- **Linux Wayland**: `wl-copy`
|
|
152
|
+
- **Windows**: `clip`
|
|
153
|
+
|
|
154
|
+
## Requirements
|
|
155
|
+
|
|
156
|
+
- Python >= 3.10
|
|
157
|
+
- `mcp` SDK >= 1.0.0
|
|
158
|
+
- Claude Code CLI
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# claude-handoff-manager
|
|
2
|
+
|
|
3
|
+
MCP server that automates Claude Code conversation handoffs. No more manual copy-paste when you hit the message cap.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When a Claude Code conversation reaches the message limit, it produces a handoff summary. You then have to:
|
|
8
|
+
1. Copy the handoff text
|
|
9
|
+
2. Exit / clear the conversation
|
|
10
|
+
3. Start a new session
|
|
11
|
+
4. Paste the handoff as the first message
|
|
12
|
+
|
|
13
|
+
This is tedious and breaks your flow.
|
|
14
|
+
|
|
15
|
+
## The Solution
|
|
16
|
+
|
|
17
|
+
This MCP server automates the entire handoff cycle:
|
|
18
|
+
|
|
19
|
+
1. **End of conversation**: Claude calls `save_handoff` - the message is saved to disk and copied to your clipboard
|
|
20
|
+
2. **New conversation**: Say `"continue from last handoff"` - Claude calls `load_handoff` and picks up exactly where you left off
|
|
21
|
+
|
|
22
|
+
Zero manual copy-paste.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Option A: `uvx` from Git (recommended for teams)
|
|
27
|
+
|
|
28
|
+
Push this repo to your team's Git host, then each developer adds it to their `~/.mcp.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"handoff-manager": {
|
|
34
|
+
"command": "uvx",
|
|
35
|
+
"args": ["--from", "git+https://github.com/YOUR_ORG/claude-handoff-manager", "claude-handoff-manager"],
|
|
36
|
+
"type": "stdio"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Option B: `uvx` from PyPI
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"handoff-manager": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["claude-handoff-manager"],
|
|
50
|
+
"type": "stdio"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Option C: Run directly (local development)
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"handoff-manager": {
|
|
62
|
+
"command": "python3",
|
|
63
|
+
"args": ["/path/to/handoff-manager/src/handoff_manager/server.py"],
|
|
64
|
+
"type": "stdio"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
After editing `~/.mcp.json`, **restart Claude Code** to pick up the new server.
|
|
71
|
+
|
|
72
|
+
## Tools
|
|
73
|
+
|
|
74
|
+
| Tool | What it does |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `save_handoff` | Saves handoff message to `~/.claude/handoffs/` + copies to clipboard |
|
|
77
|
+
| `load_handoff` | Loads the latest (or a specific) handoff in a new conversation |
|
|
78
|
+
| `copy_handoff_to_clipboard` | Copies a saved handoff to the system clipboard |
|
|
79
|
+
| `list_handoffs` | Lists all saved handoffs with previews |
|
|
80
|
+
| `delete_handoff` | Deletes a specific or all handoffs |
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### Saving (end of a conversation)
|
|
85
|
+
|
|
86
|
+
Claude will automatically call `save_handoff` when it generates a handoff summary at the message cap. You can also manually trigger it:
|
|
87
|
+
|
|
88
|
+
> "Save a handoff with the current context"
|
|
89
|
+
|
|
90
|
+
### Loading (start of a new conversation)
|
|
91
|
+
|
|
92
|
+
> "Continue from last handoff"
|
|
93
|
+
|
|
94
|
+
or
|
|
95
|
+
|
|
96
|
+
> "Load handoff"
|
|
97
|
+
|
|
98
|
+
Claude calls `load_handoff` and gets the full context from your previous session.
|
|
99
|
+
|
|
100
|
+
### Listing saved handoffs
|
|
101
|
+
|
|
102
|
+
> "Show me my saved handoffs"
|
|
103
|
+
|
|
104
|
+
### Clipboard
|
|
105
|
+
|
|
106
|
+
> "Copy the last handoff to my clipboard"
|
|
107
|
+
|
|
108
|
+
## Storage
|
|
109
|
+
|
|
110
|
+
Handoffs are stored as JSON files in `~/.claude/handoffs/`:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
~/.claude/handoffs/
|
|
114
|
+
20260611_143022_home-user-myproject.json
|
|
115
|
+
20260611_100515_home-user-otherproject.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each file contains:
|
|
119
|
+
- `id`: Unique identifier (timestamp + project slug)
|
|
120
|
+
- `created_at`: UTC timestamp
|
|
121
|
+
- `working_dir`: The project directory
|
|
122
|
+
- `project_name`: Human-friendly name
|
|
123
|
+
- `message`: The full handoff text
|
|
124
|
+
|
|
125
|
+
## Clipboard Support
|
|
126
|
+
|
|
127
|
+
Automatically copies to clipboard on save using:
|
|
128
|
+
- **macOS**: `pbcopy`
|
|
129
|
+
- **Linux X11**: `xclip` or `xsel`
|
|
130
|
+
- **Linux Wayland**: `wl-copy`
|
|
131
|
+
- **Windows**: `clip`
|
|
132
|
+
|
|
133
|
+
## Requirements
|
|
134
|
+
|
|
135
|
+
- Python >= 3.10
|
|
136
|
+
- `mcp` SDK >= 1.0.0
|
|
137
|
+
- Claude Code CLI
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "claude-handoff-manager"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server that automates Claude Code conversation handoffs — no more manual copy-paste when you hit the message cap."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Country Delight Engineering" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["claude", "mcp", "handoff", "claude-code", "model-context-protocol"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"mcp>=1.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
claude-handoff-manager = "handoff_manager.server:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/handoff_manager"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/AashiDutt/claude-handoff-manager"
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handoff Manager MCP Server
|
|
3
|
+
===========================
|
|
4
|
+
Automates the conversation handoff workflow in Claude Code.
|
|
5
|
+
|
|
6
|
+
When a conversation hits the message cap:
|
|
7
|
+
1. Claude calls save_handoff() with the handoff summary
|
|
8
|
+
2. User starts a new session
|
|
9
|
+
3. User says "continue from last handoff" -> load_handoff() returns the context
|
|
10
|
+
4. No more manual copy-paste
|
|
11
|
+
|
|
12
|
+
Storage: ~/.claude/handoffs/<timestamp>_<project_slug>.json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from mcp.server.fastmcp import FastMCP
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Constants
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
HANDOFFS_DIR = Path.home() / ".claude" / "handoffs"
|
|
31
|
+
HANDOFFS_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# MCP Server
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
mcp = FastMCP(
|
|
37
|
+
"handoff-manager",
|
|
38
|
+
instructions=(
|
|
39
|
+
"This server manages conversation handoffs for Claude Code. "
|
|
40
|
+
"Use save_handoff at the end of a conversation to persist the handoff "
|
|
41
|
+
"summary. Use load_handoff at the start of a new conversation to "
|
|
42
|
+
"retrieve the context. Use copy_handoff_to_clipboard to copy to the "
|
|
43
|
+
"system clipboard."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Helpers
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
def _project_slug(working_dir: str) -> str:
|
|
52
|
+
"""Create a filesystem-safe slug from a working directory path."""
|
|
53
|
+
return working_dir.strip("/").replace("/", "-").replace(" ", "_") or "default"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _handoff_path(handoff_id: str) -> Path:
|
|
57
|
+
return HANDOFFS_DIR / f"{handoff_id}.json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _list_handoff_files() -> list[Path]:
|
|
61
|
+
"""Return handoff files sorted newest-first."""
|
|
62
|
+
files = sorted(
|
|
63
|
+
HANDOFFS_DIR.glob("*.json"),
|
|
64
|
+
key=lambda p: p.stat().st_mtime,
|
|
65
|
+
reverse=True,
|
|
66
|
+
)
|
|
67
|
+
return files
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _copy_to_clipboard(text: str) -> str:
|
|
71
|
+
"""Copy text to system clipboard. Returns status message."""
|
|
72
|
+
system = platform.system()
|
|
73
|
+
try:
|
|
74
|
+
if system == "Darwin":
|
|
75
|
+
proc = subprocess.Popen(["pbcopy"], stdin=subprocess.PIPE)
|
|
76
|
+
proc.communicate(text.encode("utf-8"))
|
|
77
|
+
return "Copied to clipboard (pbcopy)."
|
|
78
|
+
elif system == "Linux":
|
|
79
|
+
for cmd in [
|
|
80
|
+
["xclip", "-selection", "clipboard"],
|
|
81
|
+
["xsel", "--clipboard", "--input"],
|
|
82
|
+
]:
|
|
83
|
+
if shutil.which(cmd[0]):
|
|
84
|
+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
|
85
|
+
proc.communicate(text.encode("utf-8"))
|
|
86
|
+
return f"Copied to clipboard ({cmd[0]})."
|
|
87
|
+
if shutil.which("wl-copy"):
|
|
88
|
+
proc = subprocess.Popen(["wl-copy"], stdin=subprocess.PIPE)
|
|
89
|
+
proc.communicate(text.encode("utf-8"))
|
|
90
|
+
return "Copied to clipboard (wl-copy)."
|
|
91
|
+
return "No clipboard tool found. Install xclip, xsel, or wl-copy."
|
|
92
|
+
elif system == "Windows":
|
|
93
|
+
proc = subprocess.Popen(["clip"], stdin=subprocess.PIPE)
|
|
94
|
+
proc.communicate(text.encode("utf-16le"))
|
|
95
|
+
return "Copied to clipboard (clip)."
|
|
96
|
+
else:
|
|
97
|
+
return f"Unsupported platform for clipboard: {system}"
|
|
98
|
+
except Exception as e:
|
|
99
|
+
return f"Clipboard copy failed: {e}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Tools
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def save_handoff(
|
|
107
|
+
message: str,
|
|
108
|
+
working_dir: str = "",
|
|
109
|
+
project_name: str = "",
|
|
110
|
+
) -> str:
|
|
111
|
+
"""Save a conversation handoff message for later retrieval.
|
|
112
|
+
|
|
113
|
+
Call this at the end of a conversation when the message cap is reached.
|
|
114
|
+
The handoff message is persisted to disk so the next conversation can
|
|
115
|
+
pick up where this one left off.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
message: The full handoff summary text (the fenced code block content).
|
|
119
|
+
working_dir: The working directory of the current project (auto-detected if empty).
|
|
120
|
+
project_name: Optional human-friendly project name for easier identification.
|
|
121
|
+
"""
|
|
122
|
+
if not message.strip():
|
|
123
|
+
return "Error: handoff message cannot be empty."
|
|
124
|
+
|
|
125
|
+
now = datetime.now(timezone.utc)
|
|
126
|
+
ts = now.strftime("%Y%m%d_%H%M%S")
|
|
127
|
+
wd = working_dir or os.getcwd()
|
|
128
|
+
slug = _project_slug(wd)
|
|
129
|
+
handoff_id = f"{ts}_{slug}"
|
|
130
|
+
|
|
131
|
+
payload = {
|
|
132
|
+
"id": handoff_id,
|
|
133
|
+
"created_at": now.isoformat(),
|
|
134
|
+
"working_dir": wd,
|
|
135
|
+
"project_name": project_name or slug,
|
|
136
|
+
"message": message,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
path = _handoff_path(handoff_id)
|
|
140
|
+
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
clip_status = _copy_to_clipboard(message)
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
f"Handoff saved successfully.\n"
|
|
146
|
+
f" ID: {handoff_id}\n"
|
|
147
|
+
f" Path: {path}\n"
|
|
148
|
+
f" Project: {payload['project_name']}\n"
|
|
149
|
+
f" Clipboard: {clip_status}\n\n"
|
|
150
|
+
f"To resume in a new conversation, the user can say:\n"
|
|
151
|
+
f' "continue from last handoff" or "load handoff"'
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@mcp.tool()
|
|
156
|
+
def load_handoff(
|
|
157
|
+
handoff_id: str = "",
|
|
158
|
+
working_dir: str = "",
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Load the most recent handoff message to continue a previous conversation.
|
|
161
|
+
|
|
162
|
+
Call this at the start of a new conversation to restore context from a
|
|
163
|
+
previous session that hit the message cap.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
handoff_id: Specific handoff ID to load. If empty, loads the most recent one.
|
|
167
|
+
working_dir: Filter handoffs by working directory. If empty, returns the most recent regardless of project.
|
|
168
|
+
"""
|
|
169
|
+
if handoff_id:
|
|
170
|
+
path = _handoff_path(handoff_id)
|
|
171
|
+
if not path.exists():
|
|
172
|
+
return f"Error: No handoff found with ID '{handoff_id}'."
|
|
173
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
174
|
+
return (
|
|
175
|
+
f"Handoff loaded (ID: {payload['id']}).\n"
|
|
176
|
+
f"Project: {payload['project_name']}\n"
|
|
177
|
+
f"Saved at: {payload['created_at']}\n"
|
|
178
|
+
f"Working dir: {payload['working_dir']}\n\n"
|
|
179
|
+
f"--- HANDOFF MESSAGE ---\n\n"
|
|
180
|
+
f"{payload['message']}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
files = _list_handoff_files()
|
|
184
|
+
if not files:
|
|
185
|
+
return "No handoffs found. Nothing to resume."
|
|
186
|
+
|
|
187
|
+
if working_dir:
|
|
188
|
+
slug = _project_slug(working_dir)
|
|
189
|
+
files = [f for f in files if slug in f.stem]
|
|
190
|
+
|
|
191
|
+
if not files:
|
|
192
|
+
return f"No handoffs found for working directory: {working_dir}"
|
|
193
|
+
|
|
194
|
+
path = files[0]
|
|
195
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
f"Handoff loaded (ID: {payload['id']}).\n"
|
|
199
|
+
f"Project: {payload['project_name']}\n"
|
|
200
|
+
f"Saved at: {payload['created_at']}\n"
|
|
201
|
+
f"Working dir: {payload['working_dir']}\n\n"
|
|
202
|
+
f"--- HANDOFF MESSAGE ---\n\n"
|
|
203
|
+
f"{payload['message']}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@mcp.tool()
|
|
208
|
+
def copy_handoff_to_clipboard(
|
|
209
|
+
handoff_id: str = "",
|
|
210
|
+
) -> str:
|
|
211
|
+
"""Copy a handoff message to the system clipboard.
|
|
212
|
+
|
|
213
|
+
Useful when you want to manually paste the handoff into another tool
|
|
214
|
+
or conversation.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
handoff_id: Specific handoff ID. If empty, copies the most recent one.
|
|
218
|
+
"""
|
|
219
|
+
if handoff_id:
|
|
220
|
+
path = _handoff_path(handoff_id)
|
|
221
|
+
if not path.exists():
|
|
222
|
+
return f"Error: No handoff found with ID '{handoff_id}'."
|
|
223
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
224
|
+
else:
|
|
225
|
+
files = _list_handoff_files()
|
|
226
|
+
if not files:
|
|
227
|
+
return "No handoffs found."
|
|
228
|
+
payload = json.loads(files[0].read_text(encoding="utf-8"))
|
|
229
|
+
|
|
230
|
+
status = _copy_to_clipboard(payload["message"])
|
|
231
|
+
return f"Handoff '{payload['id']}': {status}"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@mcp.tool()
|
|
235
|
+
def list_handoffs(
|
|
236
|
+
working_dir: str = "",
|
|
237
|
+
limit: int = 10,
|
|
238
|
+
) -> str:
|
|
239
|
+
"""List saved handoff messages.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
working_dir: Filter by working directory. If empty, lists all.
|
|
243
|
+
limit: Max number of handoffs to return (default 10).
|
|
244
|
+
"""
|
|
245
|
+
files = _list_handoff_files()
|
|
246
|
+
if not files:
|
|
247
|
+
return "No handoffs saved yet."
|
|
248
|
+
|
|
249
|
+
if working_dir:
|
|
250
|
+
slug = _project_slug(working_dir)
|
|
251
|
+
files = [f for f in files if slug in f.stem]
|
|
252
|
+
|
|
253
|
+
if not files:
|
|
254
|
+
return f"No handoffs found for: {working_dir}"
|
|
255
|
+
|
|
256
|
+
files = files[:limit]
|
|
257
|
+
lines = []
|
|
258
|
+
for f in files:
|
|
259
|
+
payload = json.loads(f.read_text(encoding="utf-8"))
|
|
260
|
+
preview = payload["message"][:80].replace("\n", " ")
|
|
261
|
+
lines.append(
|
|
262
|
+
f" [{payload['id']}]\n"
|
|
263
|
+
f" Project: {payload['project_name']}\n"
|
|
264
|
+
f" Saved: {payload['created_at']}\n"
|
|
265
|
+
f" Dir: {payload['working_dir']}\n"
|
|
266
|
+
f" Preview: {preview}..."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return f"Found {len(files)} handoff(s):\n\n" + "\n\n".join(lines)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
def delete_handoff(
|
|
274
|
+
handoff_id: str = "",
|
|
275
|
+
delete_all: bool = False,
|
|
276
|
+
) -> str:
|
|
277
|
+
"""Delete a saved handoff or all handoffs.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
handoff_id: The ID of the handoff to delete. Required unless delete_all is True.
|
|
281
|
+
delete_all: If True, deletes ALL saved handoffs. Use with caution.
|
|
282
|
+
"""
|
|
283
|
+
if delete_all:
|
|
284
|
+
files = _list_handoff_files()
|
|
285
|
+
count = len(files)
|
|
286
|
+
for f in files:
|
|
287
|
+
f.unlink()
|
|
288
|
+
return f"Deleted all {count} handoff(s)."
|
|
289
|
+
|
|
290
|
+
if not handoff_id:
|
|
291
|
+
return "Error: provide a handoff_id or set delete_all=True."
|
|
292
|
+
|
|
293
|
+
path = _handoff_path(handoff_id)
|
|
294
|
+
if not path.exists():
|
|
295
|
+
return f"Error: No handoff found with ID '{handoff_id}'."
|
|
296
|
+
|
|
297
|
+
path.unlink()
|
|
298
|
+
return f"Deleted handoff: {handoff_id}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Entry point
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
def main() -> None:
|
|
305
|
+
"""CLI entry point for the MCP server."""
|
|
306
|
+
mcp.run(transport="stdio")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|