[project] name = "cpersona" # Derived from cpersona/__init__.py (see [tool.hatch.version] below), so the # wheel's metadata and the version the running server reports come from one # string instead of two that have to be kept equal. dynamic = ["version"] # No tool count in this sentence, or anywhere else a reader sees one. The number # was right on every page scripts/check-docs-facts.py could reach and wrong on the # surfaces it could not — the GitHub repository description, the registry's # frozen per-version copy, a code comment — twice (2026-08-31, 2026-09-05). The # gate now fails on a count appearing at all. The GitHub description carries this # sentence and is not a file in this tree, so changing this line means also running: # gh repo edit Cloto-dev/cpersona --description "" description = "CPersona — persistent memory for AI agents over MCP. One SQLite file you own, hybrid vector + full-text + keyword search, no LLM in the loop, and honest reporting when recall degrades. MIT licensed." requires-python = ">=3.11" license = {text = "MIT"} readme = "README.md" keywords = [ "mcp", "model-context-protocol", "memory", "persistent-memory", "semantic-search", "vector-search", "rag", "llm", "agent", "claude", ] # PyPI renders these as the sidebar filters (License / Topic / Intended # Audience). The field was empty until 2.5.2, so the package was absent from # every faceted search on PyPI. Python versions list only what CI actually # exercises (3.11 and 3.13) plus 3.12, which lies between them; requires-python # still admits any 3.11+ interpreter. classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ # Capped below 2.0: the SDK's 2.0.0 (2026-07-28) removed the decorator # surface — Server.list_tools / Server.call_tool — that # _vendored_mcp_common/mcp_utils.py binds against, so an unbounded floor # resolves a release that fails at import. The cap is the constraint the # code actually satisfies, not a freeze: lifting it is the migration to # the 2.0 handler API, and that lives upstream in the shared mcp_common # this package vendors, not here. # # The floor is where the code stops working, not a preference: the HTTP # transport passes max_request_body_size to the session manager (bug-805), # an argument that first exists in 1.29.0, and pip keeps an installed mcp # that still satisfies the floor, so `pip install -U cpersona` over an # older SDK would fail at HTTP startup. 1.30.0 rather than 1.29.0 because # it is the release the lock tests, and the first outside the session- # accumulation advisory GHSA-84m7-p3x7-pcfv. This server is not exposed # to that one (its HTTP transport keeps no sessions), but an install that # carries the flagged SDK is reported as vulnerable all the same. "mcp>=1.30.0,<2", # The other three carry the same kind of cap for the same reason, arrived at # the other way round: `mcp` got one after an unbounded floor had already # resolved a breaking release, and a published wheel's `requires_dist` is # not editable afterwards. So the bound is a ceiling on a major that does # not exist yet, not a claim about a version anyone has tested. aiosqlite # and httpx are pre-1.0, where 1.0 is where each project has said the # breaking changes go; numpy's 2.0 removed public API, so <3 is the same # bet one major later. Raising any of them is a normal change: widen the # cap, let wheel-smoke resolve it unlocked, ship. "aiosqlite>=0.20.0,<1.0", "httpx>=0.27.0,<1.0", "numpy>=1.24.0,<3", # OAuth token verification (cpersona/oauth.py) imports this directly. It # already arrives as a dependency of `mcp`, so declaring it changes nothing # about what gets installed — which is the point: a package that imports a # module it does not declare keeps working only for as long as somebody # else's dependency list happens to carry it, and a published wheel's # requires_dist cannot be corrected afterwards. The crypto extra is the # part that matters (RS256/ES256 need it). The floor is the first release # that fixes the key-confusion and token-parsing advisories published # against 2.13.0 and earlier: `mcp` itself accepts older releases, so # without it `pip install -U cpersona` leaves a vulnerable PyJWT in place. # The ceiling is the usual bet on a major that does not exist yet. "pyjwt[crypto]>=2.14.0,<3", # reconstruct's modes cap the cl100k_base tokens of the JSON they return # (cpersona/tokens.py). A cap that holds only when an optional package # happens to be installed is not a cap, so the tokenizer is required. The # vocabulary ships in cpersona/vocab/, so nothing is fetched at run time. # The floor is the first release whose cl100k_base split pattern is the one # tokens.py carries (unchanged through 0.14.0) and the first with wheels # for Python 3.13; the ceiling is the usual bet on a major. "tiktoken>=0.8.0,<1", ] [project.urls] # Rendered in the PyPI sidebar. Issues is the one that closes the loop: a user # who installs from PyPI and hits a defect otherwise has no path back here. # Changelog points at Releases because this repository keeps no CHANGELOG.md — # the release notes are the changelog. Funding is a label PyPI knows: it is # rendered with its own icon rather than as another anonymous link. Homepage = "https://github.com/Cloto-dev/cpersona" Repository = "https://github.com/Cloto-dev/cpersona" Issues = "https://github.com/Cloto-dev/cpersona/issues" Changelog = "https://github.com/Cloto-dev/cpersona/releases" Documentation = "https://cloto-dev.github.io/CPersona/" Funding = "https://github.com/sponsors/Cloto-dev" [dependency-groups] # Test toolchain (installed by `uv sync --dev`; used locally and in CI). dev = [ "pytest>=8", "pytest-asyncio>=0.24", "pytest-timeout>=2.3", ] # Mutation-testing engine for the advisory mutation-diff lane (L1). # A separate group so a bare `uv sync --dev` stays lean — only the # mutation-diff CI job (and local mutation runs) pull cosmic-ray's ~28 deps. mutation = [ "cosmic-ray>=8.4.6", ] [tool.pytest.ini_options] # A bare `pytest` must never hang. Two failure modes did so historically and are the # reason the suite was rarely run (so embedding-path regressions reached production): # a lingering queue-worker, and any embedding call to a non-resident endpoint. A per-test # timeout turns either into a loud failure instead of an unbounded block, making the # suite safe to run on every change. The signal method (POSIX) interrupts blocking # syscalls; the dev and CI platforms are macOS/Linux. timeout = 60 timeout_method = "signal" asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" addopts = "-ra" # The suite lives in tests/. Pinning collection there keeps a bare `pytest` from # wandering into scratch dirs or vendored code (a stray root-level file with a # stdin read once aborted collection of the whole suite). testpaths = ["tests"] [project.scripts] cpersona = "cpersona.server:run" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.version] path = "cpersona/__init__.py" [tool.hatch.build.targets.wheel] # Package layout: the server modules live under cpersona/, including the # vendored mcp_common snapshot at cpersona/_vendored_mcp_common/ (see # scripts/sync-vendored-mcp-common.sh). The root server.py is a shim for the # git-clone / marketplace path and is deliberately excluded from the wheel. packages = ["cpersona"] # bug-181: the vendored snapshot is taken wholesale from upstream mgp-py # (sync-vendored-mcp-common.sh rsyncs the tree with --delete, and the a2 # reconciliation gate compares it byte for byte), but cpersona imports only part # of it. The unimported remainder — ~2,479 lines, including an LLM provider that # makes outbound third-party calls — has no import path from any cpersona module, # so it can only enlarge the audited and supply-chain surface on every user's disk. # Excluding it HERE and not in the tree keeps the snapshot byte-faithful, so the # sync contract and its gate are untouched. Gate 13 in tests/test_structural_gates.py # fails CI if any excluded module ever becomes reachable (a wheel missing an imported # module would raise ImportError in the field, not in our suite). exclude = [ "cpersona/_vendored_mcp_common/llm_provider.py", "cpersona/_vendored_mcp_common/mcp_stream_interceptor.py", "cpersona/_vendored_mcp_common/mgp_utils.py", "cpersona/_vendored_mcp_common/search.py", "cpersona/_vendored_mcp_common/semantic_cache.py", ] [tool.hatch.build.targets.wheel.force-include] # Ship the agent skills with the PyPI artifact (2.5.0b1): pip users previously # had no way to obtain the skill without cloning the repo. Placed under the # package so it imports nothing and pollutes no top-level namespace. The source # lives in the agent plugin (2.6.12); the installed path is unchanged. "plugin/skills" = "cpersona/skills" [tool.hatch.build.targets.sdist] # Anchored patterns: a bare "skills" is a gitignore-style glob and would also # match .agents/skills/ (local delegation tooling that must not ship). include = ["/cpersona", "/plugin/skills", "/README.md", "/LICENSE", "/pyproject.toml"] [tool.ruff.lint] # 2.5.0b2 (bug-114 class prevention): flake8-datetimez bans naive datetime # CONSTRUCTION statically — the parse-side naive-as-UTC guard in # _parse_timestamp_utc handles values already in the DB; DTZ keeps new naive # values from being written by Python code in the first place. The default # rule set (E4, E7, E9, F) is preserved via extend-select. extend-select = ["DTZ"] [tool.ruff.lint.per-file-ignores] # Tests deliberately construct naive datetimes (e.g. to exercise the bug-114 # naive-guard); benchmarks and scripts are operator-run, not server code. "tests/*" = ["DTZ"] "benchmarks/*" = ["DTZ"] "scripts/*" = ["DTZ"]