handoff-mcp-server 0.24.2 → 0.24.4
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +12 -4
- package/package.json +1 -1
- package/src/main.rs +10 -4
- package/src/setup.rs +216 -17
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -86,8 +86,8 @@ edit. Disable anytime with `/plugin disable handoff-mcp-hooks@handoff-mcp-market
|
|
|
86
86
|
the MCP server and skills remain active.
|
|
87
87
|
|
|
88
88
|
> **Important:** The hooks require a `handoff` MCP server entry in the
|
|
89
|
-
> project's `.mcp.json`.
|
|
90
|
-
>
|
|
89
|
+
> project's `.mcp.json`. Run `handoff-mcp setup --mcp-json` in the project
|
|
90
|
+
> directory to add it automatically, or add it manually:
|
|
91
91
|
>
|
|
92
92
|
> ```json
|
|
93
93
|
> {
|
|
@@ -313,8 +313,16 @@ For non-plugin installs, run:
|
|
|
313
313
|
handoff-mcp setup
|
|
314
314
|
```
|
|
315
315
|
|
|
316
|
-
This installs Claude Code hooks into `~/.claude/settings.json`
|
|
317
|
-
|
|
316
|
+
This installs Claude Code hooks into `~/.claude/settings.json` and adds a
|
|
317
|
+
`handoff` server entry to the project's `.mcp.json` (required for hooks to
|
|
318
|
+
connect). The command is interactive by default — use `-y` to skip prompts:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
handoff-mcp setup -y # non-interactive (auto-approve everything)
|
|
322
|
+
handoff-mcp setup --mcp-json # only add .mcp.json entry (skip hooks)
|
|
323
|
+
handoff-mcp setup --check # check if hooks and .mcp.json are configured
|
|
324
|
+
```
|
|
325
|
+
|
|
318
326
|
Restart Claude Code after running setup.
|
|
319
327
|
|
|
320
328
|
You can check the current status or remove the hooks:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-mcp-server",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.4",
|
|
4
4
|
"description": "MCP server that gives AI coding agents persistent memory across sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AlphaElements <66808803+alphaelements@users.noreply.github.com>",
|
package/src/main.rs
CHANGED
|
@@ -29,7 +29,11 @@ fn main() {
|
|
|
29
29
|
"setup" => {
|
|
30
30
|
let check = args.iter().any(|a| a == "--check");
|
|
31
31
|
let uninstall = args.iter().any(|a| a == "--uninstall");
|
|
32
|
-
|
|
32
|
+
let mcp_json = args.iter().any(|a| a == "--mcp-json");
|
|
33
|
+
let yes = args.iter().any(|a| a == "-y" || a == "--yes");
|
|
34
|
+
if let Err(e) =
|
|
35
|
+
handoff_mcp::setup::run_setup_with_opts(check, uninstall, mcp_json, yes)
|
|
36
|
+
{
|
|
33
37
|
eprintln!("Error: {e:#}");
|
|
34
38
|
std::process::exit(1);
|
|
35
39
|
}
|
|
@@ -136,9 +140,11 @@ SERVER MODE:
|
|
|
136
140
|
handoff-mcp Start the MCP server (stdio transport)
|
|
137
141
|
|
|
138
142
|
SETUP:
|
|
139
|
-
handoff-mcp setup
|
|
140
|
-
handoff-mcp setup --check
|
|
141
|
-
handoff-mcp setup --uninstall
|
|
143
|
+
handoff-mcp setup Install hooks + add handoff server to .mcp.json
|
|
144
|
+
handoff-mcp setup --check Check if hooks and .mcp.json are configured
|
|
145
|
+
handoff-mcp setup --uninstall Remove handoff hooks from Claude Code
|
|
146
|
+
handoff-mcp setup --mcp-json Add handoff server to .mcp.json (non-interactive)
|
|
147
|
+
handoff-mcp setup -y Skip all confirmation prompts
|
|
142
148
|
|
|
143
149
|
OPTIONS:
|
|
144
150
|
-h, --help Print this help message
|
package/src/setup.rs
CHANGED
|
@@ -14,6 +14,65 @@ fn settings_path() -> Result<PathBuf> {
|
|
|
14
14
|
Ok(Path::new(&home).join(".claude").join("settings.json"))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
fn mcp_json_path() -> PathBuf {
|
|
18
|
+
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
19
|
+
cwd.join(".mcp.json")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn build_mcp_server_entry() -> Value {
|
|
23
|
+
serde_json::json!({
|
|
24
|
+
"type": "stdio",
|
|
25
|
+
"command": "handoff-mcp",
|
|
26
|
+
"args": [],
|
|
27
|
+
"env": {}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn ensure_mcp_json_entry(path: &Path) -> Result<bool> {
|
|
32
|
+
let mut root = if path.exists() {
|
|
33
|
+
let text = std::fs::read_to_string(path)
|
|
34
|
+
.with_context(|| format!("failed to read {}", path.display()))?;
|
|
35
|
+
serde_json::from_str::<Value>(&text)
|
|
36
|
+
.with_context(|| format!("failed to parse {}", path.display()))?
|
|
37
|
+
} else {
|
|
38
|
+
serde_json::json!({ "mcpServers": {} })
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let servers = root
|
|
42
|
+
.as_object_mut()
|
|
43
|
+
.context(".mcp.json root is not an object")?
|
|
44
|
+
.entry("mcpServers")
|
|
45
|
+
.or_insert_with(|| Value::Object(serde_json::Map::new()))
|
|
46
|
+
.as_object_mut()
|
|
47
|
+
.context(".mcp.json 'mcpServers' is not an object")?;
|
|
48
|
+
|
|
49
|
+
if servers.contains_key(HOOK_SERVER) {
|
|
50
|
+
return Ok(false);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
servers.insert(HOOK_SERVER.to_string(), build_mcp_server_entry());
|
|
54
|
+
|
|
55
|
+
let text = serde_json::to_string_pretty(&root)?;
|
|
56
|
+
std::fs::write(path, text + "\n")
|
|
57
|
+
.with_context(|| format!("failed to write {}", path.display()))?;
|
|
58
|
+
Ok(true)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn has_mcp_json_entry(path: &Path) -> bool {
|
|
62
|
+
if !path.exists() {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
let Ok(text) = std::fs::read_to_string(path) else {
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
let Ok(root) = serde_json::from_str::<Value>(&text) else {
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
root.get("mcpServers")
|
|
72
|
+
.and_then(|s| s.get(HOOK_SERVER))
|
|
73
|
+
.is_some()
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
fn read_settings(path: &Path) -> Result<Value> {
|
|
18
77
|
if path.exists() {
|
|
19
78
|
let text = std::fs::read_to_string(path)
|
|
@@ -145,6 +204,15 @@ fn strip_legacy_session_start_hook(hooks_obj: &mut serde_json::Map<String, Value
|
|
|
145
204
|
}
|
|
146
205
|
|
|
147
206
|
pub fn run_setup(check_only: bool, uninstall: bool) -> Result<()> {
|
|
207
|
+
run_setup_with_opts(check_only, uninstall, false, false)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pub fn run_setup_with_opts(
|
|
211
|
+
check_only: bool,
|
|
212
|
+
uninstall: bool,
|
|
213
|
+
mcp_json: bool,
|
|
214
|
+
yes: bool,
|
|
215
|
+
) -> Result<()> {
|
|
148
216
|
anyhow::ensure!(
|
|
149
217
|
!(check_only && uninstall),
|
|
150
218
|
"--check and --uninstall cannot be used together"
|
|
@@ -161,7 +229,11 @@ pub fn run_setup(check_only: bool, uninstall: bool) -> Result<()> {
|
|
|
161
229
|
return run_uninstall(&mut settings, &path);
|
|
162
230
|
}
|
|
163
231
|
|
|
164
|
-
|
|
232
|
+
if mcp_json {
|
|
233
|
+
return run_mcp_json_only();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
run_install(&mut settings, &path, yes)
|
|
165
237
|
}
|
|
166
238
|
|
|
167
239
|
fn run_check(settings: &Value, path: &Path) -> Result<()> {
|
|
@@ -197,6 +269,17 @@ fn run_check(settings: &Value, path: &Path) -> Result<()> {
|
|
|
197
269
|
all_ok = false;
|
|
198
270
|
}
|
|
199
271
|
|
|
272
|
+
let mcp_path = mcp_json_path();
|
|
273
|
+
if has_mcp_json_entry(&mcp_path) {
|
|
274
|
+
println!(" .mcp.json: handoff server configured");
|
|
275
|
+
} else if mcp_path.exists() {
|
|
276
|
+
println!(" .mcp.json: handoff server NOT configured");
|
|
277
|
+
all_ok = false;
|
|
278
|
+
} else {
|
|
279
|
+
println!(" .mcp.json: not found (hooks need it — run `handoff-mcp setup`)");
|
|
280
|
+
all_ok = false;
|
|
281
|
+
}
|
|
282
|
+
|
|
200
283
|
if all_ok {
|
|
201
284
|
println!("\nAll hooks are configured. Memory auto-injection is active.");
|
|
202
285
|
} else {
|
|
@@ -206,7 +289,33 @@ fn run_check(settings: &Value, path: &Path) -> Result<()> {
|
|
|
206
289
|
Ok(())
|
|
207
290
|
}
|
|
208
291
|
|
|
209
|
-
fn
|
|
292
|
+
fn prompt_yn(question: &str) -> bool {
|
|
293
|
+
use std::io::{self, Write};
|
|
294
|
+
print!("{question} [Y/n] ");
|
|
295
|
+
io::stdout().flush().ok();
|
|
296
|
+
let mut input = String::new();
|
|
297
|
+
if io::stdin().read_line(&mut input).is_err() {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
let trimmed = input.trim().to_lowercase();
|
|
301
|
+
trimmed.is_empty() || trimmed == "y" || trimmed == "yes"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn run_mcp_json_only() -> Result<()> {
|
|
305
|
+
let mcp_path = mcp_json_path();
|
|
306
|
+
if has_mcp_json_entry(&mcp_path) {
|
|
307
|
+
println!(" .mcp.json: handoff server already configured. Nothing to do.");
|
|
308
|
+
return Ok(());
|
|
309
|
+
}
|
|
310
|
+
let added = ensure_mcp_json_entry(&mcp_path)?;
|
|
311
|
+
if added {
|
|
312
|
+
println!(" .mcp.json: added handoff server entry");
|
|
313
|
+
println!("\nRestart Claude Code for changes to take effect.");
|
|
314
|
+
}
|
|
315
|
+
Ok(())
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn run_install(settings: &mut Value, path: &Path, yes: bool) -> Result<()> {
|
|
210
319
|
let obj = settings
|
|
211
320
|
.as_object_mut()
|
|
212
321
|
.context("settings.json root is not an object")?;
|
|
@@ -247,11 +356,6 @@ fn run_install(settings: &mut Value, path: &Path) -> Result<()> {
|
|
|
247
356
|
installed += 1;
|
|
248
357
|
}
|
|
249
358
|
|
|
250
|
-
// Migrate away from a pre-fix install: an older `handoff-mcp setup` used
|
|
251
|
-
// to write a synchronous SessionStart `handoff_memory_cleanup` hook,
|
|
252
|
-
// which is the confirmed trigger for the VSCode hang under many
|
|
253
|
-
// parallel sub-agents (wiki/100-stdio-concurrency.md). Strip it here so
|
|
254
|
-
// that simply re-running `setup` remediates existing installs.
|
|
255
359
|
let migrated = strip_legacy_session_start_hook(hooks_obj);
|
|
256
360
|
if migrated {
|
|
257
361
|
println!(
|
|
@@ -269,11 +373,37 @@ fn run_install(settings: &mut Value, path: &Path) -> Result<()> {
|
|
|
269
373
|
if migrated {
|
|
270
374
|
println!("Legacy SessionStart cleanup hook removed. See README for migration details.");
|
|
271
375
|
}
|
|
272
|
-
println!("\nRestart Claude Code for hooks to take effect.");
|
|
273
376
|
} else {
|
|
274
|
-
println!("\nAll hooks already installed.
|
|
377
|
+
println!("\nAll hooks already installed.");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let mcp_path = mcp_json_path();
|
|
381
|
+
let needs_mcp = !has_mcp_json_entry(&mcp_path);
|
|
382
|
+
|
|
383
|
+
if needs_mcp {
|
|
384
|
+
let should_write = if yes {
|
|
385
|
+
true
|
|
386
|
+
} else {
|
|
387
|
+
println!();
|
|
388
|
+
prompt_yn("Add handoff server to .mcp.json? (required for hooks)")
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if should_write {
|
|
392
|
+
let added = ensure_mcp_json_entry(&mcp_path)?;
|
|
393
|
+
if added {
|
|
394
|
+
println!(" .mcp.json: added handoff server entry");
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
println!(
|
|
398
|
+
" .mcp.json: skipped. Hooks will not work without a handoff server.\n \
|
|
399
|
+
Run `handoff-mcp setup --mcp-json` later, or add it manually."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
println!(" .mcp.json: handoff server already configured");
|
|
275
404
|
}
|
|
276
405
|
|
|
406
|
+
println!("\nRestart Claude Code for changes to take effect.");
|
|
277
407
|
Ok(())
|
|
278
408
|
}
|
|
279
409
|
|
|
@@ -370,7 +500,7 @@ mod tests {
|
|
|
370
500
|
let path = dir.path().join("settings.json");
|
|
371
501
|
|
|
372
502
|
let mut settings = Value::Object(serde_json::Map::new());
|
|
373
|
-
run_install(&mut settings, &path).unwrap();
|
|
503
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
374
504
|
|
|
375
505
|
let written: Value =
|
|
376
506
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -393,7 +523,7 @@ mod tests {
|
|
|
393
523
|
}
|
|
394
524
|
});
|
|
395
525
|
|
|
396
|
-
run_install(&mut settings, &path).unwrap();
|
|
526
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
397
527
|
|
|
398
528
|
let written: Value =
|
|
399
529
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -408,10 +538,10 @@ mod tests {
|
|
|
408
538
|
let path = dir.path().join("settings.json");
|
|
409
539
|
|
|
410
540
|
let mut settings = Value::Object(serde_json::Map::new());
|
|
411
|
-
run_install(&mut settings, &path).unwrap();
|
|
541
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
412
542
|
|
|
413
543
|
let mut settings2 = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
414
|
-
run_install(&mut settings2, &path).unwrap();
|
|
544
|
+
run_install(&mut settings2, &path, false).unwrap();
|
|
415
545
|
|
|
416
546
|
let written: Value =
|
|
417
547
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -429,7 +559,7 @@ mod tests {
|
|
|
429
559
|
let path = dir.path().join("settings.json");
|
|
430
560
|
|
|
431
561
|
let mut settings = Value::Object(serde_json::Map::new());
|
|
432
|
-
run_install(&mut settings, &path).unwrap();
|
|
562
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
433
563
|
|
|
434
564
|
let mut settings2: Value =
|
|
435
565
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -459,7 +589,7 @@ mod tests {
|
|
|
459
589
|
std::fs::write(&path, original).unwrap();
|
|
460
590
|
|
|
461
591
|
let mut settings: Value = serde_json::from_str(original).unwrap();
|
|
462
|
-
run_install(&mut settings, &path).unwrap();
|
|
592
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
463
593
|
|
|
464
594
|
let written = std::fs::read_to_string(&path).unwrap();
|
|
465
595
|
let env_pos = written.find("\"env\"").unwrap();
|
|
@@ -500,7 +630,7 @@ mod tests {
|
|
|
500
630
|
}
|
|
501
631
|
});
|
|
502
632
|
|
|
503
|
-
run_install(&mut settings, &path).unwrap();
|
|
633
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
504
634
|
|
|
505
635
|
let written: Value =
|
|
506
636
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -527,7 +657,7 @@ mod tests {
|
|
|
527
657
|
}
|
|
528
658
|
});
|
|
529
659
|
|
|
530
|
-
run_install(&mut settings, &path).unwrap();
|
|
660
|
+
run_install(&mut settings, &path, false).unwrap();
|
|
531
661
|
|
|
532
662
|
let written: Value =
|
|
533
663
|
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
@@ -583,4 +713,73 @@ mod tests {
|
|
|
583
713
|
assert_eq!(user_prompt.len(), 1);
|
|
584
714
|
assert!(!has_handoff_hook(&written["hooks"]["UserPromptSubmit"]));
|
|
585
715
|
}
|
|
716
|
+
|
|
717
|
+
#[test]
|
|
718
|
+
fn ensure_mcp_json_creates_file_when_absent() {
|
|
719
|
+
let dir = tempfile::tempdir().unwrap();
|
|
720
|
+
let path = dir.path().join(".mcp.json");
|
|
721
|
+
assert!(!path.exists());
|
|
722
|
+
|
|
723
|
+
let added = ensure_mcp_json_entry(&path).unwrap();
|
|
724
|
+
assert!(added);
|
|
725
|
+
assert!(path.exists());
|
|
726
|
+
|
|
727
|
+
let root: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
728
|
+
assert!(root["mcpServers"]["handoff"]["command"]
|
|
729
|
+
.as_str()
|
|
730
|
+
.unwrap()
|
|
731
|
+
.contains("handoff-mcp"));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
#[test]
|
|
735
|
+
fn ensure_mcp_json_adds_to_existing_file() {
|
|
736
|
+
let dir = tempfile::tempdir().unwrap();
|
|
737
|
+
let path = dir.path().join(".mcp.json");
|
|
738
|
+
std::fs::write(
|
|
739
|
+
&path,
|
|
740
|
+
r#"{"mcpServers":{"context7":{"type":"stdio","command":"npx"}}}"#,
|
|
741
|
+
)
|
|
742
|
+
.unwrap();
|
|
743
|
+
|
|
744
|
+
let added = ensure_mcp_json_entry(&path).unwrap();
|
|
745
|
+
assert!(added);
|
|
746
|
+
|
|
747
|
+
let root: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
748
|
+
assert!(root["mcpServers"].get("context7").is_some());
|
|
749
|
+
assert!(root["mcpServers"].get("handoff").is_some());
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#[test]
|
|
753
|
+
fn ensure_mcp_json_skips_when_already_present() {
|
|
754
|
+
let dir = tempfile::tempdir().unwrap();
|
|
755
|
+
let path = dir.path().join(".mcp.json");
|
|
756
|
+
std::fs::write(
|
|
757
|
+
&path,
|
|
758
|
+
r#"{"mcpServers":{"handoff":{"type":"stdio","command":"handoff-mcp"}}}"#,
|
|
759
|
+
)
|
|
760
|
+
.unwrap();
|
|
761
|
+
|
|
762
|
+
let added = ensure_mcp_json_entry(&path).unwrap();
|
|
763
|
+
assert!(!added);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
#[test]
|
|
767
|
+
fn has_mcp_json_entry_checks_correctly() {
|
|
768
|
+
let dir = tempfile::tempdir().unwrap();
|
|
769
|
+
|
|
770
|
+
let absent = dir.path().join("absent.json");
|
|
771
|
+
assert!(!has_mcp_json_entry(&absent));
|
|
772
|
+
|
|
773
|
+
let present = dir.path().join("present.json");
|
|
774
|
+
std::fs::write(&present, r#"{"mcpServers":{"handoff":{"type":"stdio"}}}"#).unwrap();
|
|
775
|
+
assert!(has_mcp_json_entry(&present));
|
|
776
|
+
|
|
777
|
+
let no_handoff = dir.path().join("other.json");
|
|
778
|
+
std::fs::write(
|
|
779
|
+
&no_handoff,
|
|
780
|
+
r#"{"mcpServers":{"context7":{"type":"stdio"}}}"#,
|
|
781
|
+
)
|
|
782
|
+
.unwrap();
|
|
783
|
+
assert!(!has_mcp_json_entry(&no_handoff));
|
|
784
|
+
}
|
|
586
785
|
}
|