Skip to content

mcp

Resumable metadata discovery with a private local SQLite cache.

The builder writes only the configured cache; MCP consumers open it read-only. Session data is interpreted by the existing neuro_py loaders.

IndexBuilder

Build an incremental metadata cache, with resumable directory discovery.

Parameters:

Name Type Description Default
path Path

Private local SQLite output. Never place it in a recording directory.

required
max_source_bytes int

Maximum size of each MAT source to load, default 128 MB. Larger sources remain discoverable with explicit incomplete-metadata status.

128000000
exclude_dirs list of str

Additional directory names to skip, matched case-insensitively. System recycling, snapshot, metadata-cache, and development folders are skipped.

None
Source code in neuro_py/mcp/index.py
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
class IndexBuilder:
    """Build an incremental metadata cache, with resumable directory discovery.

    Parameters
    ----------
    path : pathlib.Path
        Private local SQLite output. Never place it in a recording directory.
    max_source_bytes : int, optional
        Maximum size of each MAT source to load, default 128 MB. Larger sources
        remain discoverable with explicit incomplete-metadata status.
    exclude_dirs : list of str, optional
        Additional directory names to skip, matched case-insensitively. System
        recycling, snapshot, metadata-cache, and development folders are skipped.
    """

    def __init__(
        self,
        path: Path,
        max_source_bytes: int = 128_000_000,
        exclude_dirs: list[str] | None = None,
    ) -> None:
        if max_source_bytes < 1:
            raise ValueError("max_source_bytes must be positive")
        self.path = path.resolve()
        self.max_source_bytes = max_source_bytes
        self.exclude_dirs = EXCLUDED_DIRS | {
            name.casefold() for name in exclude_dirs or []
        }

    def _initialize(self) -> None:
        with self._connect() as connection:
            version = connection.execute("PRAGMA user_version").fetchone()[0]
            if version not in {0, SCHEMA_VERSION}:
                raise ValueError("Unsupported metadata index schema")
            connection.execute("PRAGMA journal_mode=WAL")
            connection.executescript("""
                CREATE TABLE IF NOT EXISTS sessions (
                    session_id TEXT PRIMARY KEY, basepath TEXT NOT NULL,
                    fingerprint TEXT NOT NULL, payload TEXT NOT NULL);
                CREATE TABLE IF NOT EXISTS memberships (
                    session_id TEXT NOT NULL, root TEXT NOT NULL,
                    generation TEXT NOT NULL,
                    PRIMARY KEY (session_id, root));
                CREATE TABLE IF NOT EXISTS roots (
                    root TEXT PRIMARY KEY, generation TEXT NOT NULL,
                    status TEXT NOT NULL, started_at TEXT NOT NULL, finished_at TEXT,
                    directories_seen INTEGER NOT NULL, sessions_seen INTEGER NOT NULL,
                    error_count INTEGER NOT NULL, errors TEXT NOT NULL);
                CREATE TABLE IF NOT EXISTS pending (
                    root TEXT NOT NULL, path TEXT NOT NULL, PRIMARY KEY (root, path));
                CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);
                CREATE INDEX IF NOT EXISTS pending_roots ON pending(root);
                CREATE INDEX IF NOT EXISTS session_paths ON sessions(basepath);
                CREATE INDEX IF NOT EXISTS membership_roots
                    ON memberships(root, generation);
            """)
            connection.execute(f"PRAGMA user_version={SCHEMA_VERSION}")

        if os.name != "nt":
            self.path.chmod(0o600)

    @contextmanager
    def _connect(self) -> Iterator[sqlite3.Connection]:
        connection = sqlite3.connect(self.path)
        connection.row_factory = sqlite3.Row
        try:
            with connection:
                yield connection
        finally:
            connection.close()

    @contextmanager
    def _writer_lock(self) -> Iterator[None]:
        # An OS lock survives commits and is released automatically on a crash.
        with open(str(self.path) + ".lock", "a+b") as handle:
            handle.seek(0)
            if not handle.read(1):
                handle.write(b"0")
                handle.flush()
            handle.seek(0)
            if os.name == "nt":
                import msvcrt

                msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
            else:
                import fcntl

                fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
            try:
                yield
            finally:
                handle.seek(0)
                if os.name == "nt":
                    import msvcrt

                    msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
                else:
                    import fcntl

                    fcntl.flock(handle.fileno(), fcntl.LOCK_UN)

    def _begin(self, connection: sqlite3.Connection, root: str, restart: bool) -> str:
        row = connection.execute("SELECT * FROM roots WHERE root=?", (root,)).fetchone()
        if row is not None and row["status"] == "scanning" and not restart:
            return row["generation"]
        generation = uuid.uuid4().hex
        connection.execute("DELETE FROM pending WHERE root=?", (root,))
        connection.execute(
            "INSERT OR REPLACE INTO roots VALUES (?,?,'scanning',?,NULL,0,0,0,'[]')",
            (root, generation, _now()),
        )
        return generation

    def _error(self, connection: sqlite3.Connection, root: str, error: str) -> None:
        row = connection.execute(
            "SELECT errors FROM roots WHERE root=?", (root,)
        ).fetchone()
        errors = json.loads(row[0])
        if len(errors) < 5:
            errors.append(error[:1000])
        connection.execute(
            "UPDATE roots SET errors=?, error_count=error_count+1 WHERE root=?",
            (json.dumps(errors), root),
        )

    def _prune_pending(self, connection: sqlite3.Connection) -> None:
        # A resumed queue can contain millions of descendants of a newly
        # excluded cache. Remove them locally without visiting the share.
        prior = connection.execute(
            "SELECT value FROM settings WHERE key='discovery_policy'"
        ).fetchone()
        previous = (
            set(json.loads(prior[0]).get("excluded_directory_names", []))
            if prior
            else set()
        )
        added = self.exclude_dirs - previous
        if not added:
            return

        def excluded(root: str, path: str) -> bool:
            if root == "session-list":
                return False
            relative = path[len(root) :].replace("\\", "/").casefold()
            return bool(added.intersection(relative.split("/")))

        connection.create_function("excluded_pending", 2, excluded, deterministic=True)
        # Rebuild the queue in its original order rather than individually
        # deleting millions of rows and updating both indexes for every row.
        connection.execute(
            "CREATE TEMP TABLE retained_pending AS "
            "SELECT rowid AS queue_order, root, path FROM pending "
            "WHERE NOT excluded_pending(root, path)"
        )
        connection.execute("DELETE FROM pending")
        connection.execute(
            "INSERT INTO pending(rowid, root, path) "
            "SELECT queue_order, root, path FROM retained_pending ORDER BY queue_order"
        )
        connection.execute("DROP TABLE retained_pending")

    def _session(
        self, connection: sqlite3.Connection, path: Path, root: str, generation: str
    ) -> None:
        key = _identity(str(path))
        session_id = hashlib.sha256(key.encode()).hexdigest()[:20]
        prior = connection.execute(
            "SELECT * FROM sessions WHERE session_id=?", (session_id,)
        ).fetchone()
        try:
            observed = _observe(path)
            fingerprint = json.dumps(
                {"observed": observed, "max_source_bytes": self.max_source_bytes},
                sort_keys=True,
            )
            previous = (
                json.loads(prior["fingerprint"])
                if prior and prior["fingerprint"] != "unavailable"
                else {}
            )
            same_eligibility = all(
                (source["size_bytes"] > previous.get("max_source_bytes", 0))
                == (source["size_bytes"] > self.max_source_bytes)
                for product in ("epochs", "cells")
                if (source := observed["files"][product])["present"]
            )
            if (
                prior
                and previous.get("observed") == observed
                and same_eligibility
                and all(
                    value in {"missing_file", "source_exceeds_size_limit"}
                    for value in json.loads(prior["payload"])["errors"].values()
                )
            ):
                payload = json.loads(prior["payload"])
            else:
                payload = {
                    **observed,
                    **_extract(path, observed, self.max_source_bytes),
                }
                if _observe(path) != observed:
                    payload.update(
                        {
                            "metadata": {},
                            "status": "error",
                            "errors": {
                                "session": "Sources changed while indexing; retry."
                            },
                        }
                    )
            payload["observed_at"] = _now()
        except Exception as error:
            fingerprint = "unavailable"
            payload = {
                "status": "unavailable",
                "metadata": {},
                "files": {},
                "errors": {"session": f"{type(error).__name__}: {error}"[:1000]},
                "observed_at": _now(),
            }
        connection.execute(
            "INSERT OR REPLACE INTO sessions VALUES (?,?,?,?)",
            (
                session_id,
                str(path),
                fingerprint,
                json.dumps(_json_safe(payload), allow_nan=False),
            ),
        )
        connection.execute(
            "INSERT OR REPLACE INTO memberships VALUES (?,?,?)",
            (session_id, root, generation),
        )
        connection.execute(
            "UPDATE roots SET sessions_seen=sessions_seen+1 WHERE root=?", (root,)
        )

    def _finish(
        self, connection: sqlite3.Connection, root: str, generation: str
    ) -> None:
        # Prune only after a complete, error-free traversal. An offline subtree
        # must never be treated as evidence that its recordings were deleted.
        errors = connection.execute(
            "SELECT error_count FROM roots WHERE root=?", (root,)
        ).fetchone()[0]
        if errors == 0:
            connection.execute(
                "DELETE FROM memberships WHERE root=? AND generation<>?",
                (root, generation),
            )
            connection.execute(
                "DELETE FROM sessions WHERE session_id NOT IN "
                "(SELECT session_id FROM memberships)"
            )
        connection.execute(
            "UPDATE roots SET status=?, finished_at=? WHERE root=?",
            ("complete" if errors == 0 else "incomplete", _now(), root),
        )

    def build(
        self,
        roots: list[Path] | None = None,
        sessions: list[Path] | None = None,
        max_directories: int | None = None,
        max_seconds: float | None = None,
        restart: bool = False,
    ) -> dict[str, Any]:
        """Discover configured roots fairly, checkpointing each scanned directory.

        Parameters
        ----------
        roots : list of pathlib.Path, optional
            Approved recording roots. Links and junctions are not traversed.
        sessions : list of pathlib.Path, optional
            Session-list folders to index alongside the broader discovery roots.
        max_directories : int, optional
            Stop after this many directories in this invocation; resume next run.
        max_seconds : float, optional
            Stop between directories after this duration. An individual network
            operation or loader call can exceed the budget.
        restart : bool, optional
            Restart discovery from the roots, keeping cached session metadata.

        Returns
        -------
        dict
            Coverage status with durable pending directories and per-root errors.
        """
        if max_directories is not None and max_directories < 1:
            raise ValueError("max_directories must be positive")
        if max_seconds is not None and max_seconds <= 0:
            raise ValueError("max_seconds must be positive")
        roots = [Path(os.path.abspath(root)) for root in roots or []]
        if not roots and not sessions:
            raise ValueError("Supply discovery roots or session folders")
        for root in [*roots, *(sessions or [])]:
            # Check physical containment independently of lexical session IDs.
            if self.path.is_relative_to(root.resolve()):
                raise ValueError("The index cache must be outside recording roots")
        started = time.monotonic()
        scanned = 0
        self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
        with self._writer_lock():
            self._initialize()
            with self._connect() as connection:
                self._prune_pending(connection)
                connection.execute(
                    "INSERT OR REPLACE INTO settings VALUES (?,?)",
                    (
                        "discovery_policy",
                        json.dumps(
                            {
                                "excluded_directory_names": sorted(self.exclude_dirs),
                                "links_followed": False,
                                "max_source_bytes": self.max_source_bytes,
                            }
                        ),
                    ),
                )
                seeds = {str(root): [root] for root in roots}
                if sessions:
                    seeds["session-list"] = list(
                        dict.fromkeys(Path(os.path.abspath(path)) for path in sessions)
                    )
                generations = {}
                # Finish the current multi-root pass before refreshing roots
                # already completed in that pass. Otherwise a small budget
                # could repeatedly restart the first root and starve the rest.
                resuming_pass = not restart and any(
                    connection.execute(
                        "SELECT 1 FROM roots WHERE root=? AND status='scanning'",
                        (root,),
                    ).fetchone()
                    for root in seeds
                )
                ordered_seeds = list(seeds.items())
                if resuming_pass:

                    def progress(item: tuple[str, list[Path]]) -> int:
                        row = connection.execute(
                            "SELECT directories_seen FROM roots WHERE root=?",
                            (item[0],),
                        ).fetchone()
                        return row[0] if row else 0

                    ordered_seeds.sort(key=progress)
                for root, paths in ordered_seeds:
                    prior = connection.execute(
                        "SELECT * FROM roots WHERE root=?", (root,)
                    ).fetchone()
                    if (
                        resuming_pass
                        and prior is not None
                        and prior["status"] != "scanning"
                    ):
                        continue
                    resuming = (
                        prior is not None
                        and prior["status"] == "scanning"
                        and not restart
                    )
                    generation = self._begin(connection, root, restart)
                    generations[root] = generation
                    if not resuming:
                        connection.executemany(
                            "INSERT OR IGNORE INTO pending VALUES (?,?)",
                            [(root, str(path)) for path in paths],
                        )
                connection.commit()
                while generations:
                    for root, generation in list(generations.items()):
                        item = connection.execute(
                            "SELECT path FROM pending WHERE root=? "
                            "ORDER BY rowid LIMIT 1",
                            (root,),
                        ).fetchone()
                        if item is None:
                            self._finish(connection, root, generation)
                            del generations[root]
                            connection.commit()
                            continue
                        if (
                            max_directories is not None and scanned >= max_directories
                        ) or (
                            max_seconds is not None
                            and time.monotonic() - started >= max_seconds
                        ):
                            return MetadataIndex(self.path).status()
                        path = Path(item[0])
                        if root == "session-list":
                            self._session(connection, path, root, generation)
                        else:
                            self._directory(connection, path, root, generation)
                        connection.execute(
                            "DELETE FROM pending WHERE root=? AND path=?",
                            (root, str(path)),
                        )
                        connection.execute(
                            "UPDATE roots SET directories_seen=directories_seen+1 "
                            "WHERE root=?",
                            (root,),
                        )
                        # Commit each directory so interruptions preserve both
                        # metadata and the unvisited subtree queue.
                        connection.commit()
                        scanned += 1
                        if scanned % 25 == 0:
                            print(
                                f"Discovery progress: {scanned} directories this run",
                                file=sys.stderr,
                                flush=True,
                            )
        return MetadataIndex(self.path).status()

    def _directory(
        self,
        connection: sqlite3.Connection,
        path: Path,
        root: str,
        generation: str,
    ) -> None:
        if str(path) != root and path.name.casefold() in self.exclude_dirs:
            return
        try:
            filenames = set()
            children = []
            with os.scandir(path) as entries:
                for entry in entries:
                    if entry.is_symlink():
                        continue
                    if entry.is_dir(follow_symlinks=False):
                        info = entry.stat(follow_symlinks=False)
                        if getattr(info, "st_file_attributes", 0) & getattr(
                            stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400
                        ):
                            continue
                        if entry.name.casefold() not in self.exclude_dirs:
                            children.append((root, entry.path))
                    else:
                        filenames.add(os.path.normcase(entry.name))
            # Descendant links/junctions are skipped; only an explicit root can
            # alias a differently named recording folder.
            basename = _recording_path(path).name if str(path) == root else path.name
            if any(
                os.path.normcase(basename + suffix) in filenames
                for suffix in (SOURCES["epochs"], SOURCES["cells"])
            ):
                self._session(connection, path, root, generation)
            connection.executemany(
                "INSERT OR IGNORE INTO pending VALUES (?,?)", children
            )
        except OSError as error:
            self._error(connection, root, f"{path}: {type(error).__name__}: {error}")

build(roots=None, sessions=None, max_directories=None, max_seconds=None, restart=False)

Discover configured roots fairly, checkpointing each scanned directory.

Parameters:

Name Type Description Default
roots list of pathlib.Path

Approved recording roots. Links and junctions are not traversed.

None
sessions list of pathlib.Path

Session-list folders to index alongside the broader discovery roots.

None
max_directories int

Stop after this many directories in this invocation; resume next run.

None
max_seconds float

Stop between directories after this duration. An individual network operation or loader call can exceed the budget.

None
restart bool

Restart discovery from the roots, keeping cached session metadata.

False

Returns:

Type Description
dict

Coverage status with durable pending directories and per-root errors.

Source code in neuro_py/mcp/index.py
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
def build(
    self,
    roots: list[Path] | None = None,
    sessions: list[Path] | None = None,
    max_directories: int | None = None,
    max_seconds: float | None = None,
    restart: bool = False,
) -> dict[str, Any]:
    """Discover configured roots fairly, checkpointing each scanned directory.

    Parameters
    ----------
    roots : list of pathlib.Path, optional
        Approved recording roots. Links and junctions are not traversed.
    sessions : list of pathlib.Path, optional
        Session-list folders to index alongside the broader discovery roots.
    max_directories : int, optional
        Stop after this many directories in this invocation; resume next run.
    max_seconds : float, optional
        Stop between directories after this duration. An individual network
        operation or loader call can exceed the budget.
    restart : bool, optional
        Restart discovery from the roots, keeping cached session metadata.

    Returns
    -------
    dict
        Coverage status with durable pending directories and per-root errors.
    """
    if max_directories is not None and max_directories < 1:
        raise ValueError("max_directories must be positive")
    if max_seconds is not None and max_seconds <= 0:
        raise ValueError("max_seconds must be positive")
    roots = [Path(os.path.abspath(root)) for root in roots or []]
    if not roots and not sessions:
        raise ValueError("Supply discovery roots or session folders")
    for root in [*roots, *(sessions or [])]:
        # Check physical containment independently of lexical session IDs.
        if self.path.is_relative_to(root.resolve()):
            raise ValueError("The index cache must be outside recording roots")
    started = time.monotonic()
    scanned = 0
    self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    with self._writer_lock():
        self._initialize()
        with self._connect() as connection:
            self._prune_pending(connection)
            connection.execute(
                "INSERT OR REPLACE INTO settings VALUES (?,?)",
                (
                    "discovery_policy",
                    json.dumps(
                        {
                            "excluded_directory_names": sorted(self.exclude_dirs),
                            "links_followed": False,
                            "max_source_bytes": self.max_source_bytes,
                        }
                    ),
                ),
            )
            seeds = {str(root): [root] for root in roots}
            if sessions:
                seeds["session-list"] = list(
                    dict.fromkeys(Path(os.path.abspath(path)) for path in sessions)
                )
            generations = {}
            # Finish the current multi-root pass before refreshing roots
            # already completed in that pass. Otherwise a small budget
            # could repeatedly restart the first root and starve the rest.
            resuming_pass = not restart and any(
                connection.execute(
                    "SELECT 1 FROM roots WHERE root=? AND status='scanning'",
                    (root,),
                ).fetchone()
                for root in seeds
            )
            ordered_seeds = list(seeds.items())
            if resuming_pass:

                def progress(item: tuple[str, list[Path]]) -> int:
                    row = connection.execute(
                        "SELECT directories_seen FROM roots WHERE root=?",
                        (item[0],),
                    ).fetchone()
                    return row[0] if row else 0

                ordered_seeds.sort(key=progress)
            for root, paths in ordered_seeds:
                prior = connection.execute(
                    "SELECT * FROM roots WHERE root=?", (root,)
                ).fetchone()
                if (
                    resuming_pass
                    and prior is not None
                    and prior["status"] != "scanning"
                ):
                    continue
                resuming = (
                    prior is not None
                    and prior["status"] == "scanning"
                    and not restart
                )
                generation = self._begin(connection, root, restart)
                generations[root] = generation
                if not resuming:
                    connection.executemany(
                        "INSERT OR IGNORE INTO pending VALUES (?,?)",
                        [(root, str(path)) for path in paths],
                    )
            connection.commit()
            while generations:
                for root, generation in list(generations.items()):
                    item = connection.execute(
                        "SELECT path FROM pending WHERE root=? "
                        "ORDER BY rowid LIMIT 1",
                        (root,),
                    ).fetchone()
                    if item is None:
                        self._finish(connection, root, generation)
                        del generations[root]
                        connection.commit()
                        continue
                    if (
                        max_directories is not None and scanned >= max_directories
                    ) or (
                        max_seconds is not None
                        and time.monotonic() - started >= max_seconds
                    ):
                        return MetadataIndex(self.path).status()
                    path = Path(item[0])
                    if root == "session-list":
                        self._session(connection, path, root, generation)
                    else:
                        self._directory(connection, path, root, generation)
                    connection.execute(
                        "DELETE FROM pending WHERE root=? AND path=?",
                        (root, str(path)),
                    )
                    connection.execute(
                        "UPDATE roots SET directories_seen=directories_seen+1 "
                        "WHERE root=?",
                        (root,),
                    )
                    # Commit each directory so interruptions preserve both
                    # metadata and the unvisited subtree queue.
                    connection.commit()
                    scanned += 1
                    if scanned % 25 == 0:
                        print(
                            f"Discovery progress: {scanned} directories this run",
                            file=sys.stderr,
                            flush=True,
                        )
    return MetadataIndex(self.path).status()

MetadataIndex

Query a local metadata snapshot without opening recording files.

Parameters:

Name Type Description Default
path Path

Existing private SQLite index created by :class:IndexBuilder.

required
Source code in neuro_py/mcp/index.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
class MetadataIndex:
    """Query a local metadata snapshot without opening recording files.

    Parameters
    ----------
    path : pathlib.Path
        Existing private SQLite index created by :class:`IndexBuilder`.
    """

    def __init__(self, path: Path) -> None:
        self.path = path.resolve(strict=True)
        with self._connect() as connection:
            version = connection.execute("PRAGMA user_version").fetchone()[0]
            if version != SCHEMA_VERSION:
                raise ValueError(
                    "Unsupported metadata index schema. Rebuild the index."
                )

    @contextmanager
    def _connect(self) -> Iterator[sqlite3.Connection]:
        connection = sqlite3.connect(self.path.as_uri() + "?mode=ro", uri=True)
        connection.row_factory = sqlite3.Row
        try:
            with connection:
                yield connection
        finally:
            connection.close()

    def _rows(self, connection: sqlite3.Connection) -> Iterator[dict[str, Any]]:
        memberships: dict[str, list[str]] = {}
        for item in connection.execute("SELECT session_id, root FROM memberships"):
            memberships.setdefault(item["session_id"], []).append(item["root"])
        for row in connection.execute("SELECT * FROM sessions ORDER BY basepath"):
            data = json.loads(row["payload"])
            data.update(
                {
                    "session_id": row["session_id"],
                    "basepath": row["basepath"],
                    "roots": sorted(memberships.get(row["session_id"], [])),
                }
            )
            yield data

    def _page_token(
        self,
        connection: sqlite3.Connection,
        parameters: dict[str, Any],
        offset: int,
        supplied: str | None,
    ) -> str:
        if offset and not supplied:
            raise ValueError("Nonzero offset requires page_token from the first page.")
        # Hash the query-visible data inside the same read transaction as the
        # result. This works with existing read-only caches and detects updates
        # from older builders or other processes, without a schema migration.
        digest = hashlib.sha256(json.dumps(parameters, sort_keys=True).encode())
        for statement in (
            "SELECT session_id, basepath, fingerprint, payload FROM sessions "
            "ORDER BY session_id",
            "SELECT session_id, root, generation FROM memberships "
            "ORDER BY session_id, root",
        ):
            digest.update(statement.encode())
            for row in connection.execute(statement):
                for value in row:
                    encoded = value.encode("utf-8")
                    digest.update(len(encoded).to_bytes(8, "big"))
                    digest.update(encoded)
        token = digest.hexdigest()
        if supplied is not None and supplied != token:
            raise ValueError(
                "Pagination token is stale or the query changed. Discard earlier "
                "pages and restart at offset=0 without page_token."
            )
        return token

    def get_entry(self, session_id: str) -> dict[str, Any] | None:
        """Resolve an indexed session ID to a registered session folder.

        Parameters
        ----------
        session_id : str
            ID returned by an index query.

        Returns
        -------
        dict or None
            Session registration or None for an unknown ID.
        """
        with self._connect() as connection:
            row = connection.execute(
                "SELECT basepath FROM sessions WHERE session_id = ?", (session_id,)
            ).fetchone()
        if row is None:
            return None
        return {
            "session_id": session_id,
            "catalog_basepath": row[0],
            "basepath": row[0],
            "basename": Path(row[0]).name,
            "registrations": [{"source": str(self.path), "metadata": {}}],
        }

    def status(self) -> dict[str, Any]:
        """Report cached discovery coverage and resumable scan progress.

        Returns
        -------
        dict
            Per-root progress, metadata completeness, and last observation times.
        """
        with self._connect() as connection:
            connection.execute("BEGIN")
            return self._status(connection)

    def _status(self, connection: sqlite3.Connection) -> dict[str, Any]:
        roots = [
            dict(row) for row in connection.execute("SELECT * FROM roots ORDER BY root")
        ]
        total = connection.execute("SELECT COUNT(*) FROM sessions").fetchone()[0]
        statuses = dict(
            connection.execute(
                "SELECT json_extract(payload, '$.status'), COUNT(*) "
                "FROM sessions GROUP BY 1"
            ).fetchall()
        )
        pending = connection.execute("SELECT COUNT(*) FROM pending").fetchone()[0]
        settings_exist = connection.execute(
            "SELECT 1 FROM sqlite_master WHERE type='table' AND name='settings'"
        ).fetchone()
        policy = (
            connection.execute(
                "SELECT value FROM settings WHERE key='discovery_policy'"
            ).fetchone()
            if settings_exist
            else None
        )
        for root in roots:
            root["errors"] = json.loads(root["errors"])
            root.pop("generation", None)
        return {
            "status": "enabled",
            "index_path": str(self.path),
            "session_count": total,
            "cached_not_live": True,
            "discovery_policy": json.loads(policy[0]) if policy else {},
            "pending_directories": pending,
            "discovery_complete": bool(roots)
            and all(
                root["status"] == "complete" and root["error_count"] == 0
                for root in roots
            ),
            "metadata_status_counts": statuses,
            "roots": roots,
        }

    def search(
        self,
        query: str = "",
        environments: list[str] | None = None,
        regions: list[str] | None = None,
        cell_type: str = "",
        products: list[str] | None = None,
        event_types: list[str] | None = None,
        include_bad_units: bool = False,
        paths: set[str] | None = None,
        offset: int = 0,
        limit: int = 50,
        recording_regions: list[str] | None = None,
        fields: list[str] | None = None,
        page_token: str | None = None,
        query_context: str = "",
    ) -> dict[str, Any]:
        """Find sessions using cached metadata and AND-combined literal filters.

        Parameters
        ----------
        query : str, optional
            Literal substring of path, animal name, or metadata labels.
        environments, regions, products, event_types : list of str, optional
            Every requested value must match. Product names are exact keys;
            other filters are case-insensitive literal substrings.
        cell_type : str, optional
            Required cell-type substring within every requested region.
        include_bad_units : bool, optional
            Count tagged bad cells for region/type matching; default False.
        paths : set of str, optional
            Normalized session-list paths to restrict scope, or None for all.
        recording_regions : list of str, optional
            Recorded region labels from session.brainRegions, independent of
            whether sorted cells exist or their metadata could be indexed.
        offset, limit : int, optional
            Page offset and row limit (1..200).
        fields : list of str, optional
            Top-level fields to return. Omit for full metadata; ["basepath"]
            returns compact results with the same filters and total count.
        page_token : str, optional
            Token returned by the first page; required for nonzero offsets.
        query_context : str, optional
            Additional caller context bound to the pagination token.

        Returns
        -------
        dict
            Matching sessions, filtered total, coverage, and pagination.
        """
        _page(pd.DataFrame(), offset, limit)
        fields = _search_fields(fields)
        if fields and "in_session_list" in fields:
            raise ValueError("in_session_list requires a Dataset catalog context")
        unknown = set(products or []) - set(INDEX_FILES)
        if unknown:
            raise ValueError(f"Unknown products: {sorted(unknown)}")
        with self._connect() as connection:
            connection.execute("BEGIN")
            token = self._page_token(
                connection,
                {
                    "method": "search",
                    "query": query,
                    "environments": environments,
                    "regions": regions,
                    "cell_type": cell_type,
                    "products": products,
                    "event_types": event_types,
                    "include_bad_units": include_bad_units,
                    "paths": sorted(paths) if paths is not None else None,
                    "recording_regions": recording_regions,
                    "fields": fields,
                    "context": query_context,
                },
                offset,
                page_token,
            )
            found = []
            total = 0
            for row in self._rows(connection):
                if paths is not None and _identity(row["basepath"]) not in paths:
                    continue
                metadata = row.get("metadata", {})
                if any(
                    not _matches(metadata.get("recording_regions", []), term)
                    for term in recording_regions or []
                ):
                    continue
                groups = [
                    g
                    for g in metadata.get("cell_groups", [])
                    if include_bad_units or not g["bad_unit"]
                ]
                if cell_type:
                    groups = [
                        g for g in groups if _matches([g["cell_type"]], cell_type)
                    ]
                if any(
                    not _matches(metadata.get("environments", []), term)
                    for term in environments or []
                ):
                    continue
                if any(
                    not _matches([g["region"] for g in groups], term)
                    for term in regions or []
                ):
                    continue
                if cell_type and not groups:
                    continue
                if any(
                    not row.get("files", {}).get(p, {}).get("present", False)
                    for p in products or []
                ):
                    continue
                if any(
                    not _matches(row.get("event_types", []), term)
                    for term in event_types or []
                ):
                    continue
                text = "\n".join(
                    [
                        row["basepath"],
                        *metadata.get("animals", []),
                        *metadata.get("environments", []),
                        *[g["region"] for g in groups],
                        *[g["cell_type"] for g in groups],
                    ]
                )
                if query.casefold() not in text.casefold():
                    continue
                selected = [
                    g
                    for g in groups
                    if not regions
                    or any(_matches([g["region"]], term) for term in regions)
                ]
                row["matching_cell_count"] = (
                    sum(g["count"] for g in selected)
                    if "cell_count_total" in metadata
                    else None
                )
                if offset <= total < offset + limit:
                    found.append(
                        {field: row.get(field) for field in fields}
                        if fields is not None
                        else row
                    )
                total += 1
            return _bounded(
                {
                    "status": "ok",
                    "coverage": self._status(connection),
                    "page_token": token,
                    "total_rows": total,
                    "offset": offset,
                    "limit": limit,
                    "next_offset": offset + limit if offset + limit < total else None,
                    "columns": fields
                    if fields is not None
                    else list(dict.fromkeys(key for row in found for key in row)),
                    "rows": found,
                }
            )

    def summarize(
        self,
        facet: str = "environment",
        paths: set[str] | None = None,
        offset: int = 0,
        limit: int = 50,
        page_token: str | None = None,
        query_context: str = "",
    ) -> dict[str, Any]:
        """Count sessions by a metadata category without accessing the shares.

        Parameters
        ----------
        facet : str, optional
            environment, region, recording_region, cell_type, animal, product,
            event_type, root, or status. Region/type counts exclude bad units.
        paths : set of str, optional
            Restrict to normalized session-list paths, or None for all sessions.
        offset, limit : int, optional
            Page offset and row limit (1..200).
        page_token : str, optional
            First-page token, required for nonzero offsets.
        query_context : str, optional
            Additional caller context bound to the pagination token.

        Returns
        -------
        dict
            Session counts per source label, unknown count, and scan coverage.
        """
        if facet not in FACETS:
            raise ValueError(f"facet must be one of {sorted(FACETS)}")
        _page(pd.DataFrame(), offset, limit)
        with self._connect() as connection:
            connection.execute("BEGIN")
            token = self._page_token(
                connection,
                {
                    "method": "summary",
                    "facet": facet,
                    "paths": sorted(paths) if paths is not None else None,
                    "context": query_context,
                },
                offset,
                page_token,
            )
            counts: Counter[str] = Counter()
            unknown = total = 0
            for row in self._rows(connection):
                if paths is not None and _identity(row["basepath"]) not in paths:
                    continue
                total += 1
                metadata = row.get("metadata", {})
                if facet in {"region", "cell_type"}:
                    labels = {
                        g[facet]
                        for g in metadata.get("cell_groups", [])
                        if not g["bad_unit"] and g[facet]
                    }
                elif facet == "product":
                    labels = {
                        key
                        for key, value in row.get("files", {}).items()
                        if value["present"]
                    }
                elif facet == "recording_region":
                    labels = set(metadata.get("recording_regions", []))
                elif facet == "status":
                    labels = {row["status"]}
                elif facet in {"root", "event_type"}:
                    labels = set(
                        row.get("roots" if facet == "root" else "event_types", [])
                    )
                else:
                    labels = set(
                        metadata.get(
                            "environments" if facet == "environment" else "animals", []
                        )
                    )
                counts.update(labels)
                unknown += not bool(labels)
            frame = pd.DataFrame(
                [
                    {"label": label, "session_count": count}
                    for label, count in sorted(counts.items())
                ],
                columns=pd.Index(["label", "session_count"]),
            )
            return _bounded(
                {
                    "status": "ok",
                    "facet": facet,
                    "sessions_in_scope": total,
                    "sessions_without_label": unknown,
                    "coverage": self._status(connection),
                    "page_token": token,
                    **_page(frame, offset, limit),
                }
            )

get_entry(session_id)

Resolve an indexed session ID to a registered session folder.

Parameters:

Name Type Description Default
session_id str

ID returned by an index query.

required

Returns:

Type Description
dict or None

Session registration or None for an unknown ID.

Source code in neuro_py/mcp/index.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def get_entry(self, session_id: str) -> dict[str, Any] | None:
    """Resolve an indexed session ID to a registered session folder.

    Parameters
    ----------
    session_id : str
        ID returned by an index query.

    Returns
    -------
    dict or None
        Session registration or None for an unknown ID.
    """
    with self._connect() as connection:
        row = connection.execute(
            "SELECT basepath FROM sessions WHERE session_id = ?", (session_id,)
        ).fetchone()
    if row is None:
        return None
    return {
        "session_id": session_id,
        "catalog_basepath": row[0],
        "basepath": row[0],
        "basename": Path(row[0]).name,
        "registrations": [{"source": str(self.path), "metadata": {}}],
    }

search(query='', environments=None, regions=None, cell_type='', products=None, event_types=None, include_bad_units=False, paths=None, offset=0, limit=50, recording_regions=None, fields=None, page_token=None, query_context='')

Find sessions using cached metadata and AND-combined literal filters.

Parameters:

Name Type Description Default
query str

Literal substring of path, animal name, or metadata labels.

''
environments list of str

Every requested value must match. Product names are exact keys; other filters are case-insensitive literal substrings.

None
regions list of str

Every requested value must match. Product names are exact keys; other filters are case-insensitive literal substrings.

None
products list of str

Every requested value must match. Product names are exact keys; other filters are case-insensitive literal substrings.

None
event_types list of str

Every requested value must match. Product names are exact keys; other filters are case-insensitive literal substrings.

None
cell_type str

Required cell-type substring within every requested region.

''
include_bad_units bool

Count tagged bad cells for region/type matching; default False.

False
paths set of str

Normalized session-list paths to restrict scope, or None for all.

None
recording_regions list of str

Recorded region labels from session.brainRegions, independent of whether sorted cells exist or their metadata could be indexed.

None
offset int

Page offset and row limit (1..200).

0
limit int

Page offset and row limit (1..200).

0
fields list of str

Top-level fields to return. Omit for full metadata; ["basepath"] returns compact results with the same filters and total count.

None
page_token str

Token returned by the first page; required for nonzero offsets.

None
query_context str

Additional caller context bound to the pagination token.

''

Returns:

Type Description
dict

Matching sessions, filtered total, coverage, and pagination.

Source code in neuro_py/mcp/index.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
def search(
    self,
    query: str = "",
    environments: list[str] | None = None,
    regions: list[str] | None = None,
    cell_type: str = "",
    products: list[str] | None = None,
    event_types: list[str] | None = None,
    include_bad_units: bool = False,
    paths: set[str] | None = None,
    offset: int = 0,
    limit: int = 50,
    recording_regions: list[str] | None = None,
    fields: list[str] | None = None,
    page_token: str | None = None,
    query_context: str = "",
) -> dict[str, Any]:
    """Find sessions using cached metadata and AND-combined literal filters.

    Parameters
    ----------
    query : str, optional
        Literal substring of path, animal name, or metadata labels.
    environments, regions, products, event_types : list of str, optional
        Every requested value must match. Product names are exact keys;
        other filters are case-insensitive literal substrings.
    cell_type : str, optional
        Required cell-type substring within every requested region.
    include_bad_units : bool, optional
        Count tagged bad cells for region/type matching; default False.
    paths : set of str, optional
        Normalized session-list paths to restrict scope, or None for all.
    recording_regions : list of str, optional
        Recorded region labels from session.brainRegions, independent of
        whether sorted cells exist or their metadata could be indexed.
    offset, limit : int, optional
        Page offset and row limit (1..200).
    fields : list of str, optional
        Top-level fields to return. Omit for full metadata; ["basepath"]
        returns compact results with the same filters and total count.
    page_token : str, optional
        Token returned by the first page; required for nonzero offsets.
    query_context : str, optional
        Additional caller context bound to the pagination token.

    Returns
    -------
    dict
        Matching sessions, filtered total, coverage, and pagination.
    """
    _page(pd.DataFrame(), offset, limit)
    fields = _search_fields(fields)
    if fields and "in_session_list" in fields:
        raise ValueError("in_session_list requires a Dataset catalog context")
    unknown = set(products or []) - set(INDEX_FILES)
    if unknown:
        raise ValueError(f"Unknown products: {sorted(unknown)}")
    with self._connect() as connection:
        connection.execute("BEGIN")
        token = self._page_token(
            connection,
            {
                "method": "search",
                "query": query,
                "environments": environments,
                "regions": regions,
                "cell_type": cell_type,
                "products": products,
                "event_types": event_types,
                "include_bad_units": include_bad_units,
                "paths": sorted(paths) if paths is not None else None,
                "recording_regions": recording_regions,
                "fields": fields,
                "context": query_context,
            },
            offset,
            page_token,
        )
        found = []
        total = 0
        for row in self._rows(connection):
            if paths is not None and _identity(row["basepath"]) not in paths:
                continue
            metadata = row.get("metadata", {})
            if any(
                not _matches(metadata.get("recording_regions", []), term)
                for term in recording_regions or []
            ):
                continue
            groups = [
                g
                for g in metadata.get("cell_groups", [])
                if include_bad_units or not g["bad_unit"]
            ]
            if cell_type:
                groups = [
                    g for g in groups if _matches([g["cell_type"]], cell_type)
                ]
            if any(
                not _matches(metadata.get("environments", []), term)
                for term in environments or []
            ):
                continue
            if any(
                not _matches([g["region"] for g in groups], term)
                for term in regions or []
            ):
                continue
            if cell_type and not groups:
                continue
            if any(
                not row.get("files", {}).get(p, {}).get("present", False)
                for p in products or []
            ):
                continue
            if any(
                not _matches(row.get("event_types", []), term)
                for term in event_types or []
            ):
                continue
            text = "\n".join(
                [
                    row["basepath"],
                    *metadata.get("animals", []),
                    *metadata.get("environments", []),
                    *[g["region"] for g in groups],
                    *[g["cell_type"] for g in groups],
                ]
            )
            if query.casefold() not in text.casefold():
                continue
            selected = [
                g
                for g in groups
                if not regions
                or any(_matches([g["region"]], term) for term in regions)
            ]
            row["matching_cell_count"] = (
                sum(g["count"] for g in selected)
                if "cell_count_total" in metadata
                else None
            )
            if offset <= total < offset + limit:
                found.append(
                    {field: row.get(field) for field in fields}
                    if fields is not None
                    else row
                )
            total += 1
        return _bounded(
            {
                "status": "ok",
                "coverage": self._status(connection),
                "page_token": token,
                "total_rows": total,
                "offset": offset,
                "limit": limit,
                "next_offset": offset + limit if offset + limit < total else None,
                "columns": fields
                if fields is not None
                else list(dict.fromkeys(key for row in found for key in row)),
                "rows": found,
            }
        )

status()

Report cached discovery coverage and resumable scan progress.

Returns:

Type Description
dict

Per-root progress, metadata completeness, and last observation times.

Source code in neuro_py/mcp/index.py
310
311
312
313
314
315
316
317
318
319
320
def status(self) -> dict[str, Any]:
    """Report cached discovery coverage and resumable scan progress.

    Returns
    -------
    dict
        Per-root progress, metadata completeness, and last observation times.
    """
    with self._connect() as connection:
        connection.execute("BEGIN")
        return self._status(connection)

summarize(facet='environment', paths=None, offset=0, limit=50, page_token=None, query_context='')

Count sessions by a metadata category without accessing the shares.

Parameters:

Name Type Description Default
facet str

environment, region, recording_region, cell_type, animal, product, event_type, root, or status. Region/type counts exclude bad units.

'environment'
paths set of str

Restrict to normalized session-list paths, or None for all sessions.

None
offset int

Page offset and row limit (1..200).

0
limit int

Page offset and row limit (1..200).

0
page_token str

First-page token, required for nonzero offsets.

None
query_context str

Additional caller context bound to the pagination token.

''

Returns:

Type Description
dict

Session counts per source label, unknown count, and scan coverage.

Source code in neuro_py/mcp/index.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
def summarize(
    self,
    facet: str = "environment",
    paths: set[str] | None = None,
    offset: int = 0,
    limit: int = 50,
    page_token: str | None = None,
    query_context: str = "",
) -> dict[str, Any]:
    """Count sessions by a metadata category without accessing the shares.

    Parameters
    ----------
    facet : str, optional
        environment, region, recording_region, cell_type, animal, product,
        event_type, root, or status. Region/type counts exclude bad units.
    paths : set of str, optional
        Restrict to normalized session-list paths, or None for all sessions.
    offset, limit : int, optional
        Page offset and row limit (1..200).
    page_token : str, optional
        First-page token, required for nonzero offsets.
    query_context : str, optional
        Additional caller context bound to the pagination token.

    Returns
    -------
    dict
        Session counts per source label, unknown count, and scan coverage.
    """
    if facet not in FACETS:
        raise ValueError(f"facet must be one of {sorted(FACETS)}")
    _page(pd.DataFrame(), offset, limit)
    with self._connect() as connection:
        connection.execute("BEGIN")
        token = self._page_token(
            connection,
            {
                "method": "summary",
                "facet": facet,
                "paths": sorted(paths) if paths is not None else None,
                "context": query_context,
            },
            offset,
            page_token,
        )
        counts: Counter[str] = Counter()
        unknown = total = 0
        for row in self._rows(connection):
            if paths is not None and _identity(row["basepath"]) not in paths:
                continue
            total += 1
            metadata = row.get("metadata", {})
            if facet in {"region", "cell_type"}:
                labels = {
                    g[facet]
                    for g in metadata.get("cell_groups", [])
                    if not g["bad_unit"] and g[facet]
                }
            elif facet == "product":
                labels = {
                    key
                    for key, value in row.get("files", {}).items()
                    if value["present"]
                }
            elif facet == "recording_region":
                labels = set(metadata.get("recording_regions", []))
            elif facet == "status":
                labels = {row["status"]}
            elif facet in {"root", "event_type"}:
                labels = set(
                    row.get("roots" if facet == "root" else "event_types", [])
                )
            else:
                labels = set(
                    metadata.get(
                        "environments" if facet == "environment" else "animals", []
                    )
                )
            counts.update(labels)
            unknown += not bool(labels)
        frame = pd.DataFrame(
            [
                {"label": label, "session_count": count}
                for label, count in sorted(counts.items())
            ],
            columns=pd.Index(["label", "session_count"]),
        )
        return _bounded(
            {
                "status": "ok",
                "facet": facet,
                "sessions_in_scope": total,
                "sessions_without_label": unknown,
                "coverage": self._status(connection),
                "page_token": token,
                **_page(frame, offset, limit),
            }
        )

main()

Build a private index from a JSON configuration or explicit CLI roots.

Source code in neuro_py/mcp/index.py
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
def main() -> None:
    """Build a private index from a JSON configuration or explicit CLI roots."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--config",
        type=Path,
        help="Private JSON with index_db, roots, catalogs, path_maps",
    )
    parser.add_argument("--index-db", type=Path)
    parser.add_argument("--root", type=Path, action="append", default=[])
    parser.add_argument("--catalog", type=Path, action="append", default=[])
    parser.add_argument("--max-directories", type=int)
    parser.add_argument("--max-seconds", type=float)
    parser.add_argument("--max-source-bytes", type=int, default=128_000_000)
    parser.add_argument("--restart", action="store_true")
    args = parser.parse_args()
    config = json.loads(args.config.read_text()) if args.config else {}
    index_path = args.index_db or (
        Path(config["index_db"]) if "index_db" in config else None
    )
    if index_path is None:
        parser.error("Supply --index-db or index_db in --config")
    roots = [*args.root, *[Path(value) for value in config.get("roots", [])]]
    catalogs = [*args.catalog, *[Path(value) for value in config.get("catalogs", [])]]
    sessions = []
    if catalogs:
        from neuro_py.mcp.dataset import Dataset

        dataset = Dataset(
            catalogs=catalogs,
            path_maps=[tuple(pair) for pair in config.get("path_maps", [])],
        )
        sessions = [Path(entry["basepath"]) for entry in dataset._sessions.values()]
    result = IndexBuilder(
        index_path, args.max_source_bytes, exclude_dirs=config.get("exclude_dirs", [])
    ).build(roots, sessions, args.max_directories, args.max_seconds, args.restart)
    print(json.dumps(result, indent=2))