handoff-mcp 0.3.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.
- handoff_mcp-0.3.0/.gitattributes +12 -0
- handoff_mcp-0.3.0/.github/workflows/ci.yml +84 -0
- handoff_mcp-0.3.0/.github/workflows/release.yml +56 -0
- handoff_mcp-0.3.0/.gitignore +29 -0
- handoff_mcp-0.3.0/CHANGELOG.md +110 -0
- handoff_mcp-0.3.0/LICENSE +21 -0
- handoff_mcp-0.3.0/PKG-INFO +531 -0
- handoff_mcp-0.3.0/README.md +496 -0
- handoff_mcp-0.3.0/benchmarks/RESULTS.md +87 -0
- handoff_mcp-0.3.0/benchmarks/brief_reconstruction.py +248 -0
- handoff_mcp-0.3.0/benchmarks/consolidation_eval.py +232 -0
- handoff_mcp-0.3.0/benchmarks/supersession_benchmark.py +174 -0
- handoff_mcp-0.3.0/docs/adr/0001-sqlite-not-vectordb.md +49 -0
- handoff_mcp-0.3.0/docs/adr/0002-deterministic-brief.md +44 -0
- handoff_mcp-0.3.0/docs/adr/0003-markdown-as-source-of-truth.md +47 -0
- handoff_mcp-0.3.0/docs/adr/0004-optional-semantic-layer.md +59 -0
- handoff_mcp-0.3.0/docs/adr/0005-incremental-index-sync.md +50 -0
- handoff_mcp-0.3.0/docs/adr/0006-memory-consolidation.md +57 -0
- handoff_mcp-0.3.0/docs/adr/0007-supersede-by-best-match.md +67 -0
- handoff_mcp-0.3.0/docs/adr/0008-honest-benchmark.md +70 -0
- handoff_mcp-0.3.0/docs/architecture.md +126 -0
- handoff_mcp-0.3.0/docs/architecture.svg +74 -0
- handoff_mcp-0.3.0/examples/demo_presentation.py +150 -0
- handoff_mcp-0.3.0/examples/stdio_smoke.py +55 -0
- handoff_mcp-0.3.0/examples/two_sessions_demo.py +123 -0
- handoff_mcp-0.3.0/pyproject.toml +93 -0
- handoff_mcp-0.3.0/scripts/check_tool_surface.py +84 -0
- handoff_mcp-0.3.0/server.json +37 -0
- handoff_mcp-0.3.0/skills/handoff/SKILL.md +97 -0
- handoff_mcp-0.3.0/skills/planning/SKILL.md +67 -0
- handoff_mcp-0.3.0/src/handoff_mcp/__init__.py +24 -0
- handoff_mcp-0.3.0/src/handoff_mcp/brief.py +196 -0
- handoff_mcp-0.3.0/src/handoff_mcp/cli_import.py +68 -0
- handoff_mcp-0.3.0/src/handoff_mcp/cli_init.py +83 -0
- handoff_mcp-0.3.0/src/handoff_mcp/cli_sync.py +35 -0
- handoff_mcp-0.3.0/src/handoff_mcp/config.py +94 -0
- handoff_mcp-0.3.0/src/handoff_mcp/embeddings.py +209 -0
- handoff_mcp-0.3.0/src/handoff_mcp/engine.py +351 -0
- handoff_mcp-0.3.0/src/handoff_mcp/importers.py +213 -0
- handoff_mcp-0.3.0/src/handoff_mcp/index.py +323 -0
- handoff_mcp-0.3.0/src/handoff_mcp/models.py +168 -0
- handoff_mcp-0.3.0/src/handoff_mcp/py.typed +0 -0
- handoff_mcp-0.3.0/src/handoff_mcp/search.py +153 -0
- handoff_mcp-0.3.0/src/handoff_mcp/semantic.py +242 -0
- handoff_mcp-0.3.0/src/handoff_mcp/server.py +385 -0
- handoff_mcp-0.3.0/src/handoff_mcp/summarizer.py +128 -0
- handoff_mcp-0.3.0/src/handoff_mcp/supersession.py +57 -0
- handoff_mcp-0.3.0/src/handoff_mcp/sync.py +205 -0
- handoff_mcp-0.3.0/src/handoff_mcp/vault.py +378 -0
- handoff_mcp-0.3.0/tests/conftest.py +30 -0
- handoff_mcp-0.3.0/tests/test_benchmark.py +50 -0
- handoff_mcp-0.3.0/tests/test_brief.py +172 -0
- handoff_mcp-0.3.0/tests/test_cli_init.py +43 -0
- handoff_mcp-0.3.0/tests/test_cli_sync.py +42 -0
- handoff_mcp-0.3.0/tests/test_concurrency.py +63 -0
- handoff_mcp-0.3.0/tests/test_consolidation.py +172 -0
- handoff_mcp-0.3.0/tests/test_embedding_backends.py +137 -0
- handoff_mcp-0.3.0/tests/test_entities.py +58 -0
- handoff_mcp-0.3.0/tests/test_importers.py +87 -0
- handoff_mcp-0.3.0/tests/test_index_sync.py +110 -0
- handoff_mcp-0.3.0/tests/test_mcp_integration.py +270 -0
- handoff_mcp-0.3.0/tests/test_models.py +49 -0
- handoff_mcp-0.3.0/tests/test_path_safety.py +32 -0
- handoff_mcp-0.3.0/tests/test_rerank.py +81 -0
- handoff_mcp-0.3.0/tests/test_search_crossproject.py +79 -0
- handoff_mcp-0.3.0/tests/test_search_fusion.py +41 -0
- handoff_mcp-0.3.0/tests/test_semantic.py +179 -0
- handoff_mcp-0.3.0/tests/test_session_lifecycle.py +54 -0
- handoff_mcp-0.3.0/tests/test_supersede_by_match.py +84 -0
- handoff_mcp-0.3.0/tests/test_supersession.py +43 -0
- handoff_mcp-0.3.0/tests/test_sync.py +236 -0
- handoff_mcp-0.3.0/tests/test_vault.py +133 -0
- handoff_mcp-0.3.0/uv.lock +2851 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ci-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: py${{ matrix.python-version }} on ${{ matrix.os }}
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os: [ubuntu-latest]
|
|
20
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
21
|
+
include:
|
|
22
|
+
# Smoke-test the markdown/path handling on Windows too.
|
|
23
|
+
- os: windows-latest
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Install uv
|
|
30
|
+
uses: astral-sh/setup-uv@v5
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
|
|
34
|
+
- name: Check the lockfile is in sync with pyproject.toml
|
|
35
|
+
run: uv lock --check
|
|
36
|
+
|
|
37
|
+
- name: Install project into a venv
|
|
38
|
+
run: |
|
|
39
|
+
uv venv --python ${{ matrix.python-version }}
|
|
40
|
+
uv pip install -e ".[dev]"
|
|
41
|
+
|
|
42
|
+
- name: Ruff (lint + format check)
|
|
43
|
+
run: |
|
|
44
|
+
uv run --no-sync ruff check .
|
|
45
|
+
uv run --no-sync ruff format --check .
|
|
46
|
+
|
|
47
|
+
- name: Mypy (strict)
|
|
48
|
+
run: uv run --no-sync mypy
|
|
49
|
+
|
|
50
|
+
- name: Pytest
|
|
51
|
+
run: uv run --no-sync pytest
|
|
52
|
+
|
|
53
|
+
- name: Benchmark smoke run
|
|
54
|
+
run: |
|
|
55
|
+
uv run --no-sync python benchmarks/brief_reconstruction.py
|
|
56
|
+
uv run --no-sync python benchmarks/supersession_benchmark.py
|
|
57
|
+
|
|
58
|
+
tool-surface:
|
|
59
|
+
name: MCP tool-surface smoke gate
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
|
|
64
|
+
- name: Install uv
|
|
65
|
+
uses: astral-sh/setup-uv@v5
|
|
66
|
+
with:
|
|
67
|
+
enable-cache: true
|
|
68
|
+
|
|
69
|
+
- name: Install project into a venv
|
|
70
|
+
run: |
|
|
71
|
+
uv venv --python 3.12
|
|
72
|
+
uv pip install -e .
|
|
73
|
+
|
|
74
|
+
# Drive the real MCP JSON-RPC surface through the Inspector and assert the
|
|
75
|
+
# schemas + annotations clients depend on. Inspector is pinned so this gate
|
|
76
|
+
# tracks *our* contract, not upstream churn. tools/list needs no provider —
|
|
77
|
+
# just a writable vault dir (the server's ensure_dirs() creates it).
|
|
78
|
+
- name: Assert tool schemas + annotations (MCP Inspector)
|
|
79
|
+
env:
|
|
80
|
+
HANDOFF_VAULT: ${{ runner.temp }}/handoff-vault
|
|
81
|
+
run: |
|
|
82
|
+
npx --yes @modelcontextprotocol/inspector@0.22.0 --cli \
|
|
83
|
+
uv run --no-sync handoff-mcp --method tools/list \
|
|
84
|
+
| python scripts/check_tool_surface.py
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-driven PyPI release via trusted publishing (no API tokens):
|
|
4
|
+
# push a `vX.Y.Z` tag and the matching version is built and published.
|
|
5
|
+
# Requires the "pypi" trusted publisher to be configured for this repo on
|
|
6
|
+
# https://pypi.org/manage/account/publishing/ (environment: pypi).
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: Build sdist + wheel
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
|
|
22
|
+
- name: Check the tag matches the package version
|
|
23
|
+
run: |
|
|
24
|
+
version=$(uvx --from toml-cli toml get --toml-path pyproject.toml project.version)
|
|
25
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
26
|
+
if [ "$version" != "$tag" ]; then
|
|
27
|
+
echo "Tag v$tag does not match pyproject version $version" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
- name: Build distributions
|
|
32
|
+
run: uv build
|
|
33
|
+
|
|
34
|
+
- uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
name: Publish to PyPI (trusted publishing)
|
|
41
|
+
needs: build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/handoff-mcp
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write # OIDC token for PyPI trusted publishing
|
|
48
|
+
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/download-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
- name: Publish package distributions to PyPI
|
|
56
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtualenv / uv
|
|
10
|
+
.venv/
|
|
11
|
+
.venv*/
|
|
12
|
+
# uv.lock IS committed: CI runs `uv lock --check` and the package is published to
|
|
13
|
+
# PyPI, so the resolved lockfile belongs in the repo for reproducible builds.
|
|
14
|
+
|
|
15
|
+
# Tooling caches
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
|
|
20
|
+
# Local vault / derived index (runtime data, never source of truth in repo)
|
|
21
|
+
*.handoff-vault/
|
|
22
|
+
*.db
|
|
23
|
+
*.db-shm
|
|
24
|
+
*.db-wal
|
|
25
|
+
|
|
26
|
+
# OS / editor
|
|
27
|
+
.DS_Store
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.3.0] — 2026-07-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Multi-device sync** (`sync` tool + `handoff-sync` CLI) — a thin, opt-in git
|
|
11
|
+
layer over the vault: pull, commit, and push a private git remote so memory
|
|
12
|
+
follows you across machines. `handoff-sync --setup <url>` configures the remote
|
|
13
|
+
(writing a union-merge `.gitattributes` and gitignoring the derived index); set
|
|
14
|
+
`HANDOFF_AUTO_SYNC=1` for pull-on-brief / push-on-checkpoint. Core memory stays
|
|
15
|
+
fully functional with zero git.
|
|
16
|
+
- Entity-note conflicts across machines resolve automatically via git's built-in
|
|
17
|
+
`union` merge driver — no custom merge engine.
|
|
18
|
+
- **Tool-surface CI smoke gate** (`scripts/check_tool_surface.py`) pinning the MCP
|
|
19
|
+
tool schemas and read-only/destructive annotations against drift.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Git subprocesses are bounded by a timeout and refuse interactive credential
|
|
23
|
+
prompts, so a slow or unauthenticated remote can never block a session.
|
|
24
|
+
|
|
25
|
+
## [0.2.0] — 2026-07-06
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **Memory consolidation** (`consolidate` tool) — opt-in LLM "sleep" pass that
|
|
29
|
+
distils old finished sessions' active decisions into durable entity notes and
|
|
30
|
+
archives the originals, to tackle memory *volume* at scale (ADR-0006).
|
|
31
|
+
- **Deterministic reranking** of recall — `search_memory` reorders results by a
|
|
32
|
+
relevance + recency (time-decay) + importance blend; `rerank=False` for raw
|
|
33
|
+
order.
|
|
34
|
+
- **Importers** — `handoff-import` CLI bootstraps a project's memory from a git
|
|
35
|
+
commit history (deterministic) or a Claude Code transcript (best-effort);
|
|
36
|
+
idempotent re-import.
|
|
37
|
+
- **Pluggable embedding backends** — `hashing` (default, zero-dep), `openai`
|
|
38
|
+
(OpenAI-compatible incl. a self-hosted proxy), `local` (sentence-transformers, default
|
|
39
|
+
model `Qwen/Qwen3-Embedding-0.6B`); hybrid keyword+semantic via RRF (ADR-0004).
|
|
40
|
+
- **Incremental, persistent index** — startup syncs only changed session notes
|
|
41
|
+
by `(mtime, size)` signature, so cost scales with new work, not history
|
|
42
|
+
(ADR-0005).
|
|
43
|
+
- Benchmarks (offline, deterministic): `brief_reconstruction` (brief vs naive
|
|
44
|
+
dump) and `supersession_benchmark` (supersession on vs off, in isolation). See
|
|
45
|
+
`benchmarks/RESULTS.md`.
|
|
46
|
+
- **Supersede-by-best-match** — `log_event(supersedes_query=…)` retires the best-
|
|
47
|
+
matching active event of the same type when the caller doesn't have its id;
|
|
48
|
+
deterministic, auditable, opt-in (ADR-0007).
|
|
49
|
+
- **Tool annotations** — every tool now advertises `ToolAnnotations` to the host:
|
|
50
|
+
`get_brief`/`search_memory` are read-only, `consolidate` is destructive (it
|
|
51
|
+
archives sessions) and the only open-world tool (it may call an external LLM).
|
|
52
|
+
- `search_memory` results include each event's **id** (feed it straight into
|
|
53
|
+
`log_event(supersedes=…)` to retire a stale decision you just found) and the
|
|
54
|
+
tool exposes a `limit` parameter (default 10).
|
|
55
|
+
- **MCP registry manifest** (`server.json`) and a tag-driven **PyPI release
|
|
56
|
+
workflow** using trusted publishing (`.github/workflows/release.yml`).
|
|
57
|
+
- Python **3.14** support (CI matrix, classifiers).
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- Retired the misleading `supersession_benchmark` head-to-head vs mem0 (keyword
|
|
61
|
+
retrieval masked the mechanism, an empty answer scored as a win, and it wasn't
|
|
62
|
+
reproducible offline); reframed the benchmarks honestly around the two above
|
|
63
|
+
(ADR-0008).
|
|
64
|
+
- Pinned the MCP SDK to `mcp>=1.27,<2` — mcp 2.0 is a breaking rewrite
|
|
65
|
+
(`FastMCP` → `MCPServer`); the v2 migration will be deliberate, not accidental.
|
|
66
|
+
- `log_event(type=…)` and `search_memory(scope=…)` are now `Literal`-typed, so
|
|
67
|
+
the published schemas are enum-constrained: invalid values (e.g. a `'curent'`
|
|
68
|
+
typo) are rejected by the protocol layer instead of being silently coerced.
|
|
69
|
+
- `__version__` is sourced from package metadata (`importlib.metadata`) instead
|
|
70
|
+
of a hardcoded string, and the server no longer pokes the SDK's private
|
|
71
|
+
`_mcp_server.version` attribute.
|
|
72
|
+
|
|
73
|
+
### Concurrency
|
|
74
|
+
- The shared SQLite connection (keyword index + semantic store) is now serialised
|
|
75
|
+
by a reentrant lock, fixing interleaved-statement errors and lost writes when an
|
|
76
|
+
MCP transport dispatches tool calls from worker threads.
|
|
77
|
+
|
|
78
|
+
### Security
|
|
79
|
+
- Validate `project` / `session_id` and sanitise entity names to prevent path
|
|
80
|
+
traversal / arbitrary file writes outside the vault (covered by
|
|
81
|
+
`tests/test_path_safety.py::test_safe_segment_rejects_traversal` and the
|
|
82
|
+
engine-level `test_engine_rejects_unsafe_project`, which exercises the
|
|
83
|
+
actual tool entry point).
|
|
84
|
+
- Strip ASCII control characters (except tab/newlines) from event content and
|
|
85
|
+
link targets — `\x1f` is the index's packed-list separator and could corrupt
|
|
86
|
+
the `links` column.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
- Vault round-trip no longer loses or mis-attributes events whose content spans
|
|
90
|
+
lines or contains `<!-- -->` / lookalike metadata.
|
|
91
|
+
- Cross-project supersession is resolved globally (briefs and search).
|
|
92
|
+
- Hardened SQLite (WAL + busy timeout) for concurrent sessions; atomic note
|
|
93
|
+
writes; zero-vector handling consistent across the vec0 and brute-force paths.
|
|
94
|
+
- Index timestamps are normalised to UTC before storage: `ORDER BY created_at`
|
|
95
|
+
compares ISO strings lexicographically, so mixed offsets (e.g. imported git
|
|
96
|
+
history) could order the brief wrongly. `rebuild` self-heals existing indexes.
|
|
97
|
+
- `consolidate` archives sessions **before** appending distilled facts, so a
|
|
98
|
+
crash between the two steps can no longer duplicate every fact on the next
|
|
99
|
+
run (it merely loses regenerable facts — the sessions stay archived, intact).
|
|
100
|
+
|
|
101
|
+
## [0.1.0] — 2026-06-29
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
- Initial release: FastMCP server with `log_event`, `get_brief`, `search_memory`,
|
|
105
|
+
`checkpoint`, `note_entity`; `session://brief` resource and `resume` prompt.
|
|
106
|
+
- Markdown vault as the source of truth with a derived SQLite + FTS5 index.
|
|
107
|
+
- Temporal supersession (retracted decisions excluded from briefs and recall).
|
|
108
|
+
- Deterministic, token-budgeted, graph-aware hand-off brief.
|
|
109
|
+
- Cross-project memory: project-scoped briefs, global `search_memory`.
|
|
110
|
+
- ADRs, architecture docs, two-session demo, and a Claude Code skill.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Solrange
|
|
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.
|