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
8 changes: 6 additions & 2 deletions src/vouch/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,14 +1096,18 @@ def kb_doctor() -> dict[str, Any]:


@mcp.tool()
def kb_export(out_path: str) -> dict[str, Any]:
def kb_export(out_path: str, exclude: list[str] | None = None) -> dict[str, Any]:
# exclude: subdir/file names to omit (e.g. "decided", "sessions") for a
# knowledge-only bundle — mirrors `vouch export --exclude` and the kb.export
# jsonl rpc, which both already accept it; this surface had dropped it.
s = _store()
dest = bundle.fenced_bundle_path(s, out_path)
manifest = bundle.export(s.kb_dir, dest=dest, actor=_agent())
manifest = bundle.export(s.kb_dir, dest=dest, actor=_agent(), exclude=tuple(exclude or ()))
return {
"bundle_id": manifest["bundle_id"],
"files": len(manifest["files"]),
"out": out_path,
"excluded": manifest["excluded"],
}


Expand Down
20 changes: 20 additions & 0 deletions tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,26 @@ def test_export_exclude_filters_subdirs(store: KBStore, tmp_path: Path) -> None:
assert m_filt["bundle_id"] != m_full["bundle_id"]


def test_kb_export_mcp_honors_exclude(store: KBStore, monkeypatch: pytest.MonkeyPatch) -> None:
# the mcp kb_export tool must accept `exclude` like the jsonl rpc and cli
# export do, so an agent (the primary mcp client) can produce a knowledge-only
# bundle. this surface silently dropped the filter.
from vouch.server import kb_export

src = store.put_source(b"e", title="doc")
store.put_claim(Claim(id="c1", text="alpha", evidence=[src.id]))
_write_session_file(store)
monkeypatch.chdir(store.root)

result = kb_export("knowledge.tar.gz", exclude=["decided", "sessions"])

assert result["excluded"] == ["decided", "sessions"]
with tarfile.open(store.root / "knowledge.tar.gz", "r:gz") as tar:
names = [m.name for m in tar.getmembers()]
assert not any(n.startswith("sessions/") for n in names)
assert any(n.startswith("claims/") for n in names)


def test_filtered_bundle_imports_cleanly(store: KBStore, tmp_path: Path) -> None:
src = store.put_source(b"e", title="doc")
store.put_claim(Claim(id="c1", text="alpha", evidence=[src.id]))
Expand Down
Loading