Skip to main content

deco-assaying

MCP server that performs tree-sitter-based source code analysis. Designed to feed structural information about a repo (symbols, imports, references, chunks, metrics) into a downstream consumer that maintains a knowledge base over many codebases.

Run

Five ways to run it. Pick whichever matches your situation.

Mode When to use
1. uvx (one-off) Try it once, no install.
2. uv tool install (pinned daemon) Run it occasionally, want it on $PATH.
3. macOS LaunchAgent Persistent daemon on a Mac.
4. Linux systemd user unit Persistent daemon on Linux.
5. Docker / docker compose Container deployment, ops stack, shared host.

Prereqs

  • uv-based modes (1–4) need uv and git. uv ships a portable Python 3.13, so no system Python install required.

    curl -LsSf https://astral.sh/uv/install.sh | sh
    
  • Docker mode (5) needs docker (or compatible). The image bundles Python 3.13 and git; nothing else on the host.

In every mode the server listens on PORT (default 35832). Sanity-check it's up:

curl http://127.0.0.1:35832/health

To serve MCP over stdio instead of HTTP, run deco-assaying --transport stdio (--transport {http,stdio}; the default is http).


1. One-off — uvx

uvx resolves the package into a temporary venv and runs it once. Nothing persists between runs.

uvx deco-assaying                       # latest release
uvx deco-assaying@0.3.7                 # pin a specific version

# With env vars (e.g. private-repo token, custom output dir):
PUBLIC_BASE_URL=http://localhost:35832 \
GITHUB_TOKEN=ghp_xxx \
OUTPUT_ROOT=$HOME/da-output \
  uvx deco-assaying

Good for kicking the tires or running on a CI box where you don't want to leave anything on disk.

2. Pinned daemon — uv tool install

Installs the deco-assaying command on your $PATH, isolated in its own venv that uv manages. Faster startup than uvx (no resolve on each run).

uv tool install deco-assaying
deco-assaying                           # foreground server

To upgrade later: uv tool upgrade deco-assaying. To remove: uv tool uninstall deco-assaying.

To run in the background as your user (no init system):

nohup deco-assaying > ~/da.log 2>&1 &

For a real "always running" setup, see the launchd / systemd recipes below.

3. macOS persistent daemon (launchd)

After uv tool install deco-assaying, register a LaunchAgent so the daemon starts at login and restarts if it crashes.

Save this as ~/Library/LaunchAgents/ai.parkviewlab.deco-assaying.plist (replace CHANGE-ME with your username):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>ai.parkviewlab.deco-assaying</string>

  <key>ProgramArguments</key>
  <array>
    <string>/Users/CHANGE-ME/.local/bin/deco-assaying</string>
  </array>

  <key>EnvironmentVariables</key>
  <dict>
    <key>OUTPUT_ROOT</key>
    <string>/Users/CHANGE-ME/deco-assaying-output</string>
    <key>PUBLIC_BASE_URL</key>
    <string>http://localhost:35832</string>
    <!-- Uncomment for private-repo access:
    <key>GITHUB_TOKEN</key>
    <string>ghp_xxx</string>
    -->
  </dict>

  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>

  <key>StandardOutPath</key>
  <string>/Users/CHANGE-ME/Library/Logs/deco-assaying.out.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/CHANGE-ME/Library/Logs/deco-assaying.err.log</string>
</dict>
</plist>

Load and start it:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.parkviewlab.deco-assaying.plist
launchctl kickstart  -k gui/$(id -u)/ai.parkviewlab.deco-assaying

# Check status:
launchctl print gui/$(id -u)/ai.parkviewlab.deco-assaying | head -30

# Tail logs:
tail -f ~/Library/Logs/deco-assaying.{out,err}.log

# Stop / unload:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.parkviewlab.deco-assaying.plist

4. Linux persistent daemon (systemd)

After uv tool install deco-assaying, register a user-scope systemd unit so no root is required.

Save this as ~/.config/systemd/user/deco-assaying.service:

[Unit]
Description=deco-assaying MCP server
After=network-online.target

[Service]
Type=simple
ExecStart=%h/.local/bin/deco-assaying
Restart=on-failure
RestartSec=5
Environment=OUTPUT_ROOT=%h/deco-assaying-output
Environment=PUBLIC_BASE_URL=http://localhost:35832
# Uncomment for private-repo access:
# Environment=GITHUB_TOKEN=ghp_xxx
# Environment=GITLAB_TOKEN=glpat-xxx

[Install]
WantedBy=default.target

Enable and start:

systemctl --user daemon-reload
systemctl --user enable --now deco-assaying

# Check status:
systemctl --user status deco-assaying

# Tail logs:
journalctl --user -u deco-assaying -f

# Stop:
systemctl --user disable --now deco-assaying

To keep the daemon running when the user is logged out, enable lingering:

loginctl enable-linger "$USER"

5. Docker / GHCR

Pull the published multi-arch image (linux/amd64 + linux/arm64) and run it directly:

docker pull ghcr.io/parkviewlab/deco-assaying:latest

docker run --rm \
  -p 35832:35832 \
  -e PUBLIC_BASE_URL=http://localhost:35832 \
  -v deco-assaying-data:/data \
  ghcr.io/parkviewlab/deco-assaying:latest

Pin a specific version with a tag — :0.3.7, :0.3, or :latest. See the container registry for available tags.

For a real deployment, copy docker-compose.yml, edit the CHANGE-ME placeholders (most importantly PUBLIC_BASE_URL), then:

docker compose up -d                    # start in background
docker compose logs -f                  # tail logs
docker compose pull && docker compose up -d   # upgrade
docker compose down                     # stop, keep volume
docker compose down -v                  # stop and drop the volume

The compose file shows two volume options: a docker-managed named volume (default) or a bind mount to a host path you can browse directly. Switch by commenting / uncommenting the relevant lines.

For a Portainer stack, paste the contents of docker-compose.yml into the stack editor (lowercase stack name — Portainer rejects caps).

From source (for development)

git clone https://github.com/parkviewlab/deco-assaying.git
cd deco-assaying
uv sync
uv run python -m deco_assaying

Endpoints

  • POST /sse — MCP Streamable HTTP transport. Tools, prompts.
  • GET /health — liveness probe.
  • GET /admin/* — read-only JSON ops endpoints.
  • GET /outputs/{job_id}/... — read-only download API for job artifacts.
  • GET /docs — OpenAPI / Swagger UI for the HTTP API.

HTTP responses are gzipped when the client sends Accept-Encoding: gzip (transparent for any modern client).

MCP tools

The MCP server exposes a small surface; descriptions in the tools' schemas explain the recommended order of use. Highlights:

  • analyze_file(content, filename?, language?, ...) — analyze ONE file whose source you already have, inline. Don't use this to analyze a whole repo.
  • index_repo(source, git_ref?, max_file_bytes?, max_partial_clone_bytes?, ...) — start an async indexing job. Returns {job_id}. source can be a local directory, a GitHub URL, or a GitLab URL (including nested groups).
  • get_job_status(job_id) — poll until state == "done".
  • cancel_job(job_id) — cooperative cancel.
  • get_manifest(job_id) — repo-level summary. Read first.
  • get_analysis_index(job_id) — sizes + absolute download URLs for every analysis file. Read second to plan which artifacts to fetch and which (if any) to side-route via a fetch tool to avoid context-window overflow.
  • get_top_level_symbols(job_id, ...) — module-level definitions only. The cheap default for understanding repo shape.
  • get_all_symbols(job_id, ...) — every definition (including methods
    • nested classes). Heavier; use for cross-cutting "find every X" queries.
  • get_tree(job_id, path_prefix?, analyzed_only?) — full path inventory with size totals.
  • get_languages(job_id) / get_errors(job_id) — small rollups.
  • list_job_files(job_id, glob?) — paths of every per-file artifact.
  • get_file_analysis(job_id, path, sections?) — drill into one file's analysis. Pass sections= to skip the chunks payload.
  • get_log_events(job_id, ...) — tail the run log (during or after).
  • list_supported_languages() / detect_language(path) — capability helpers.

MCP prompts (workflow templates)

The server ships two prompts so any client picking it up inherits the recommended workflow without reading this README:

  • analyze_repo(source, focus?) — full lifecycle from scratch.
  • explore_finished_job(job_id, question) — focused question against a finished job.

Both explain the URL-fallback strategy: when an artifact is too big for the context window, an agent paired with a generic HTTP fetch tool can hand its absolute url (from analysis_index.json) to out-of-band processing instead of inlining the raw content.

Output download API

Every job's artifacts land under OUTPUT_ROOT/{job_id}/. A consumer sharing the volume can read them off disk; one without a shared volume pulls them over HTTP:

Endpoint Returns
GET /outputs/{job_id} manifest.json (convenience).
GET /outputs/{job_id}/manifest.json Repo-level rollup.
GET /outputs/{job_id}/analysis_index.json Sizes + absolute URLs for every artifact.
GET /outputs/{job_id}/tree.json Full path inventory (analyzed + skipped).
GET /outputs/{job_id}/all_symbols.json Every definition.
GET /outputs/{job_id}/top_level_symbols.json Module-level definitions only.
GET /outputs/{job_id}/languages.json Per-language counts.
GET /outputs/{job_id}/errors.json Parse errors + skipped files.
GET /outputs/{job_id}/log.jsonl?from_offset=N Tail the job's log.
GET /outputs/{job_id}/ls?path=&recursive= Directory listing.
GET /outputs/{job_id}/file/{path} Single file, or a streaming ZIP if any path segment contains *?[. E.g. /file/files/**/*.py.json.
GET /outputs/{job_id}/zip?path=&match= Explicit-bulk-zip alias. Default = whole job dir.
DELETE /outputs/{job_id} Remove the dir + drop the table entry. 409 if still running.
GET /admin/outputs List every job_id present on disk under OUTPUT_ROOT.

Path traversal (.., absolute paths, escape via symlink) is rejected.

Resource requirements

When index_repo runs against a GitHub or GitLab URL, the server takes one of two exclusive paths, chosen after a provider pre-flight (see Network below) that lists every blob with its size. The threshold max_partial_clone_bytes is a top-level argument of index_repo:

  • Streaming, no clone. If the pre-flight succeeds and the planned source exceeds max_partial_clone_bytes (default 100 MB), nothing is cloned. The server packs the files into batches totaling at most max_partial_clone_bytes, downloads each batch through the provider's raw URL (16 threads) into output_path/.source/, analyzes it, deletes the batch's files, then fetches the next. Peak source-side scratch space stays at ~100 MB however large the repo is.

  • Single partial clone. Otherwise the server runs one git clone --depth=1 --filter=blob:limit=<max_file_bytes> (without the filter under eager_clone) into output_path/.source/, and the clone stays there until the job directory is purged. The ~100 MB bound holds only when the pre-flight succeeds: if it fails (for example on GitHub's unauthenticated limit of 60 requests an hour), the job clones every blob under max_file_bytes.

The other costs are the same on both paths:

  • Output artifacts: roughly 1-2× the analyzed-source size. Each analyzed file produces a JSON artifact under output_path/files/ containing symbols, imports, references, chunks, etc. These persist past the job — the consumer reads them incrementally — and are the largest durable footprint. The retention sweeper auto-purges job dirs older than OUTPUT_EXPIRY_DAYS.

  • Memory: modest. A ProcessPoolExecutor runs one worker per CPU, each holding one file's bytes plus its tree-sitter parse tree in memory. Source files are capped at max_file_bytes (default 2 MB), so worst case is ~16 MB of resident source + parse trees on an 8-core box.

  • Network: one provider-API pre-flight to choose the path and, when streaming, to plan the batches (GitHub Trees REST or GitLab REST tree + GraphQL; free for public repos, set GITHUB_TOKEN / GITLAB_TOKEN for higher quotas and private-repo access). Streaming then makes one raw-URL request per file; the single-clone path makes one git clone.

For local-path sources nothing is fetched and nothing is cloned — the only on-disk cost is the output artifacts.

Configuration

Env var Default (daemon) Default (container) Purpose
PORT 35832 35832 HTTP listen port.
HOST 0.0.0.0 0.0.0.0 HTTP bind address.
OUTPUT_ROOT ./output /data Where the server writes job dirs.
OUTPUT_EXPIRY_DAYS 7 7 Auto-purge job dirs older than this. 0 disables.
PUBLIC_BASE_URL http://localhost:${PORT} ¹ http://localhost:${PORT} ¹ Externally-reachable URL of this daemon, used to build absolute download URLs in analysis_index.json. Override in any deployment where clients reach the server at a different address.
JOB_HISTORY_MAX 100 100 In-memory job-table cap.
DEFAULT_MAX_PARTIAL_CLONE_BYTES 104857600 104857600 Default max_partial_clone_bytes: above this planned source size, a GitHub or GitLab job streams in batches of at most this many bytes instead of cloning.
GITHUB_TOKEN unset unset Optional. Raises GitHub Trees API quota from 60 to 5000 req/hr and unlocks private repos.
GITLAB_TOKEN unset unset Optional. GitLab API auth + private-repo access.

¹ The PUBLIC_BASE_URL default is built from PORT at startup. If you set PORT=8080 without setting PUBLIC_BASE_URL, the default becomes http://localhost:8080.

Releasing

Tag-driven via the Release workflow on push of a v* tag. Use the ParkviewLab/dev-tools helpers — they enforce the SSOT-tag-CI loop (pyproject.toml is the only place the version lives; the workflow's Verify tag matches the version file step would fail otherwise).

# in the deco-assaying-main worktree
git pull --ff-only                        # sync main
git -C ../deco-assaying-develop pull --ff-only   # sync develop too
git merge --no-ff develop                 # promote develop to main
git bump <patch|minor|major>              # bump the version, committed as "release v<new>"
git release                               # annotated tag v<new> from pyproject.toml
git push --follow-tags                    # CI fires
git back-merge                            # the release's last step

The steps, and the reasons for each, are in the ParkviewLab handbook's releases.md (see "Cutting a release" and "The release's last step").

The workflow runs four jobs: a gate (tag equals the pyproject version, which carries no dev marker; tag reachable from origin/main; version greater than the previous tag) gates the two publish jobs — docker (multi-arch GHCR push, X.Y.Z / X.Y / latest tags) and pypi (wheel + sdist via trusted publishing). After both publish, a changelog job generates the new CHANGELOG.md section (LLM-written "Highlights" header + categorized list written by dev-tools' generate-changelog), commits it back to main, and creates the GitHub Release with the same content as its body. ~3-5 minutes end-to-end.

Per-version release notes live in CHANGELOG.md and on the GitHub Releases page.

Don't have the helpers? Install once: git clone https://github.com/ParkviewLab/dev-tools.git ~/dev-tools && cd ~/dev-tools && ./install.sh.

Commit message convention

The changelog job categorizes commits using Conventional Commits prefixes (the full list is in the ParkviewLab handbook's commits-and-changelogs.md):

Title Group in the notes Notes
any type with ! after it (feat!:), or a breaking-change footer Breaking changes listed there once, whatever its type
feat: Features user-visible
fix: Bug fixes user-visible
perf: Performance user-visible
refactor: Refactor
docs: Docs
test: Tests
revert: Reverts GitHub's Revert button titles a PR Revert "…", which has no type
build: / chore: / ci: / style: Maintenance
any other title Other changes the whole title
a commit with no pull request Direct commits its subject and short hash

A title without a recognised type is not dropped: it is listed whole under Other changes. So prefix your PR titles, and correct a title before the merge, since retitling afterwards does not change the commit. The groups appear in the order above, and an empty group is left out.

License

Licensed under either of

at your option. In SPDX terms: MIT OR Apache-2.0.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you shall be dual-licensed as above, without any additional terms or conditions. See LICENSING.md.


© 2026 Gary Frattarola · Licensed under MIT OR Apache-2.0 · part of ParkviewLab

Metadata

Release files for deco-assaying 0.3.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deco-assaying 0.3.8
File Size Uploaded Provenance Actions
deco_assaying-0.3.8.tar.gz 183.1 kB With provenance Details for deco_assaying-0.3.8.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for deco-assaying 0.3.8
File Interpreter ABI Platform Actions
deco_assaying-0.3.8-py3-none-any.whl Python 3 none any Details for deco_assaying-0.3.8-py3-none-any.whl

Total release size: 294.6 kB

Release files / deco_assaying-0.3.8.tar.gz

Download URL deco_assaying-0.3.8.tar.gz
Size 183.1 kB
Tags Source
SHA-256 checksum
How to use checksums
85d8ff35c9d2f2d0ba0f44b8f18358d8b9e1b3209b74c3965e4fbf7a93f49e2a
BLAKE2b-256 checksum
How to use checksums
c5d5490d5985f3c40cdd62fd93c392e80339f57727ff56f0e844dda7b9396e62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / deco_assaying-0.3.8-py3-none-any.whl

Download URL deco_assaying-0.3.8-py3-none-any.whl
Size 111.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
484cd4e21d18afdb083776fc6a30f05f11c7978ece1010cd405fe3127a00095f
BLAKE2b-256 checksum
How to use checksums
af750994bca50cca3447cf6c5aaccd61ca4c55b450b2d874fdeb7cd4e9ec4cbd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Security for deco-assaying 0.3.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for deco_assaying-0.3.8.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page