Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cf8586b
feat: add user-completable MCP upload handoff
tulayha Aug 1, 2026
2e3ac10
fix: resolve upload handoff review feedback
tulayha Aug 1, 2026
73569ad
fix: complete upload handoff lifecycle
tulayha Aug 1, 2026
68674a4
fix: bind upload elicitation to user identity
tulayha Aug 1, 2026
05dd66f
feat(upload): add session-first multi-file handoff
tulayha Aug 1, 2026
1fb02b3
feat(artifacts): add transport-aware clip delivery
tulayha Aug 1, 2026
e40e4eb
fix(artifacts): preserve resource fallback and finish download page
tulayha Aug 1, 2026
99c6ef3
feat(mcp): deliver inspectable retrieval evidence
tulayha Aug 1, 2026
dd8106f
fix(mcp): complete native media ingestion
tulayha Aug 1, 2026
0655f44
refactor(upload): centralize durable ingestion coordination
tulayha Aug 2, 2026
a6c0227
fix(mcp): bound resources and complete host contracts
tulayha Aug 2, 2026
a90346d
fix(upload-ui): harden recovery and deployment guidance
tulayha Aug 2, 2026
90bed00
fix(upload): close lifecycle and delivery gaps
tulayha Aug 2, 2026
564f79c
fix(upload): enforce ingestion ownership and terminal semantics
tulayha Aug 2, 2026
b54e596
fix(upload): restore recovery and index retry semantics
tulayha Aug 2, 2026
72fe6b9
fix(mcp): close final ingestion and delivery gaps
tulayha Aug 2, 2026
f19ab37
fix(mcp): simplify upload lifecycle boundaries
tulayha Aug 2, 2026
42fc535
fix(upload): release expired tus reservations
tulayha Aug 2, 2026
4cf4f7b
chore: merge main into mcp upload handoff
tulayha Aug 2, 2026
2a36dd8
fix(mcp): name the local ingestion poll tool
tulayha Aug 2, 2026
30efbec
build(desktop): refresh runtime constraints
tulayha Aug 2, 2026
2de1a3f
build(desktop): refresh runtime manifest digest
tulayha Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,22 @@ jobs:
fi

changed_files="$(git diff --name-only "$BASE_SHA" HEAD)"
if grep -Eq '^(\.github/workflows/ci\.yml$|src/|tests/|utils/|LICENSE$|MANIFEST\.in$|pyproject\.toml$|uv\.lock$)' <<< "$changed_files"; then
if grep -Eq '^(\.github/workflows/ci\.yml$|src/|tests/|utils/|web/upload-page/|LICENSE$|MANIFEST\.in$|pyproject\.toml$|setup\.py$|uv\.lock$)' <<< "$changed_files"; then
run_suite=true
else
run_suite=false
fi
if grep -Eq '^(\.dockerignore$|\.github/workflows/ci\.yml$|Dockerfile$|compose\.yaml$|pyproject\.toml$|uv\.lock$|src/vidxp/(requirements/.*\.txt|capabilities/[^/]+/requirements\.txt)$)' <<< "$changed_files"; then
if grep -Eq '^(\.dockerignore$|\.github/workflows/ci\.yml$|Dockerfile$|compose(\.coolify)?\.yaml$|pyproject\.toml$|uv\.lock$|src/vidxp/(requirements/.*\.txt|capabilities/[^/]+/requirements\.txt)$)' <<< "$changed_files"; then
run_container=true
else
run_container=false
fi

needs_python="$run_suite"
if [[ "$run_suite" == "true" || "$run_container" == "true" ]]; then
needs_python=true
else
needs_python=false
fi
echo "run_suite=$run_suite" >> "$GITHUB_OUTPUT"
echo "run_container=$run_container" >> "$GITHUB_OUTPUT"
echo "needs_python=$needs_python" >> "$GITHUB_OUTPUT"
Expand All @@ -139,6 +143,13 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip

- uses: actions/setup-node@v7
if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14'
with:
node-version: "22"
cache: npm
cache-dependency-path: web/upload-page/package-lock.json

- name: Install repository tooling
run: |
python -m pip install --upgrade pip
Expand All @@ -162,6 +173,21 @@ jobs:
if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14'
run: ruff check src tests

- name: Install locked upload-page tooling
if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14'
run: npm ci
working-directory: web/upload-page

- name: Test upload page
if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14'
run: npm test
working-directory: web/upload-page

- name: Build upload page and verify committed bundle
if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14'
run: npm run check:bundle
working-directory: web/upload-page

- name: Test
if: needs.scope.outputs.run_suite == 'true'
run: uv run --no-sync python -m unittest discover -s tests -q
Expand Down Expand Up @@ -214,7 +240,20 @@ jobs:

- name: Validate Compose configuration
if: needs.scope.outputs.run_container == 'true' && matrix.python-version == '3.14'
run: docker compose config --quiet
env:
POSTGRES_PASSWORD: ci-postgres-password
VIDXP_ARTIFACT_DOWNLOAD_SECRET: ci-artifact-download-secret-000000
VIDXP_CONTROL_IMAGE: vidxp-control:ci
VIDXP_PUBLIC_API_HOST: api.example.test
VIDXP_UPLOAD_CLEANUP_TOKEN: ci-upload-cleanup-token-0000000000
VIDXP_UPLOAD_CORS_ORIGIN_REGEX: '^(https://api\.example\.test)$'
VIDXP_UPLOAD_HANDOFF_PUBLIC_URL: https://api.example.test/upload-handoff
VIDXP_UPLOAD_HANDOFF_SECRET: ci-upload-handoff-secret-000000000
VIDXP_UPLOAD_PUBLIC_ENDPOINT: https://uploads.example.test/uploads/
VIDXP_WORKER_IMAGE: vidxp-worker:ci
run: |
docker compose -f compose.yaml config --quiet
docker compose -f compose.coolify.yaml config --quiet

- name: Build container
if: needs.scope.outputs.run_container == 'true' && matrix.python-version == '3.14'
Expand Down
45 changes: 42 additions & 3 deletions INSTALLATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,20 @@ After installing `local-worker,mcp`:
vidxp mcp-config
```

The command prints a complete, import-ready `mcpServers` JSON object with the
resolved absolute `vidxp-mcp` executable and default repository argument.
The command prints `mcpServers` JSON for Claude Desktop and other clients that
use that local-stdio format, with the resolved absolute `vidxp-mcp` executable
and default repository argument. It is not a universal MCP configuration.

Codex uses its own configuration. Either run:

```bash
codex mcp add vidxp -- vidxp-mcp --repository default
```

or add an `[mcp_servers.vidxp]` entry to `~/.codex/config.toml`. The ChatGPT
desktop app and Codex share that local MCP configuration. ChatGPT web does not
read this file or the generated JSON; connect it to a deployed HTTPS `/mcp`
endpoint through a custom app/connector instead.

```bash
vidxp-mcp --check --repository default
Expand Down Expand Up @@ -301,9 +313,36 @@ The unauthenticated local default is deliberately loopback-only:
Native installs default to port `32191` to avoid the heavily reused development
port `8000`. Use `vidxp-api --port <port>` when a specific port is required.

The Streamable HTTP MCP endpoint includes `create_media_upload`. Its returned
capability link uses the actual listener host and port; opening `/` manually is
not an upload flow. Native mode serves the packaged Uppy page and receives bounded,
non-resumable multipart uploads directly in the API process. It requires no Docker,
PostgreSQL, Chroma server, tusd, or separately started helper. The session result
reports the effective per-file and aggregate limits, and `get_media_upload` follows
the durable import and automatic-indexing lifecycle through `indexed` and
`searchable=true`.

`vidxp-api --share` is different: it binds a bearer-protected, plain-HTTP LAN
listener but cannot safely synthesize an advertised browser handoff origin.
Consequently its MCP surface omits `create_media_upload` and
`get_media_upload` unless an explicit HTTPS
`VIDXP_UPLOAD_HANDOFF_PUBLIC_URL` is configured. The command reports that
omission instead of exposing a tool that would fail when called.

Local stdio exposes `ingest_local_media` instead. Pass one to ten paths that are
inside the configured import boundaries and poll `get_media_ingestion`; file bytes
do not cross MCP. Both ingestion tools default to the repository's advertised
capability set. Supply `modalities` to narrow it or
`index_after_import=false` for the advanced registration-only workflow.

Do not bind an unauthenticated API to a non-loopback address. Public
deployments require static bearer or OIDC authentication and should use the
supported server Compose topology.
supported server Compose topology. Hosted ChatGPT and Claude integrations
should use OIDC because those clients cannot be configured with VidXP's private
single-tenant static token. Set `VIDXP_HTTP_AUTH_MODE=oidc`, the issuer,
audience, JWKS URL, required scopes, and canonical HTTPS
`VIDXP_MCP_PUBLIC_URL`; VidXP publishes the MCP protected-resource metadata and
validates those access tokens.

## Desktop application

Expand Down
10 changes: 10 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
include README.md
include LICENSE
include docs/images/logo.png
recursive-include src/vidxp/assets/upload_page *
recursive-include src/vidxp/assets/artifact_download *
include web/upload-page/package.json
include web/upload-page/package-lock.json
include web/upload-page/scripts/build.mjs
include web/upload-page/src/app.js
include web/upload-page/src/app.css
include web/upload-page/src/index.html
include web/upload-page/src/recovery.js
prune web/upload-page/node_modules
global-exclude *.whl
global-exclude *.pyc
global-exclude __pycache__
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ vidxp doctor
vidxp mcp-config
```

`vidxp mcp-config` emits the `mcpServers` JSON used by Claude Desktop and
compatible local stdio clients. Codex has separate configuration: use
`codex mcp add vidxp -- vidxp-mcp --repository default` or configure
`[mcp_servers.vidxp]` in `~/.codex/config.toml`. ChatGPT Desktop and ChatGPT web
use ChatGPT's own connector configuration and do not read Codex's config; hosted
ChatGPT connections use a remote MCP endpoint, with VidXP deployed behind HTTPS
and OIDC.

The CLI works without MCP. Add the browser app with:

```bash
Expand All @@ -91,6 +99,11 @@ intend to expose the unauthenticated browser interface on the local network.
Streamlit prints its Local and Network URLs when it starts. VidXP disables
Streamlit's first-run email prompt and usage-statistics collection.

`vidxp-api --share` exposes a bearer-protected HTTP API/MCP endpoint on a
trusted LAN. Browser upload tools are omitted unless an explicit HTTPS upload
handoff origin is configured, because a LAN listener alone cannot advertise a
safe browser capability page.

If the `vidxp` command is not found, run `uv tool update-shell` once and reopen
the terminal.

Expand Down Expand Up @@ -134,6 +147,8 @@ See the [Coolify guide](docs/deployment/coolify.md) for the complete setup.
- Group recurring faces in a video and render a highlighted actor overlay.
- Search one selected video or every video in the active library.
- Open matching timestamps and export downloadable clips and overlays.
- Retrieve completed clips through native MCP resources, local stdio paths, or
short-lived resumable HTTPS downloads without embedding video bytes in tool JSON.
- Keep personal, client, or project libraries separate.
- Follow long indexing jobs, cancel them, and keep the last working index if a
later run fails.
Expand Down Expand Up @@ -174,6 +189,17 @@ scenes, ask questions about a library, and create clips or actor overlays.
Local agents can connect over stdio; remote agents can connect to a
self-hosted VidXP server.

Search and question results can include directly inspectable frames and clips, so
agents can show the evidence behind an answer without making users translate raw
timestamps. Evidence rendering is best-effort: a result can still be useful when
an individual frame or clip cannot be produced.

Remote agents can hand users a short-lived page for selecting and uploading
multiple videos. Local agents can ingest approved filesystem paths without moving
video bytes through MCP. VidXP normally indexes successful imports automatically;
registration-only ingestion stops at `registered`, and indexing failures can be
retried without uploading the video again.

Agents can call `get_workspace` before acting to inspect registered media,
active-index coverage, model readiness, and the searchable, queryable,
inspectable, or renderable roles available for each video. Invalid capability
Expand Down
22 changes: 19 additions & 3 deletions compose.coolify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,31 @@ x-vidxp-environment: &vidxp-environment
VIDXP_MODEL_CACHE: /var/lib/vidxp/models
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
VIDXP_HTTP_BIND_HOST: 0.0.0.0
VIDXP_HTTP_AUTH_MODE: static
VIDXP_HTTP_STATIC_BEARER_TOKEN: ${VIDXP_HTTP_STATIC_BEARER_TOKEN:?Set a bearer token of at least 32 characters}
VIDXP_HTTP_AUTH_MODE: ${VIDXP_HTTP_AUTH_MODE:-static}
VIDXP_HTTP_STATIC_BEARER_TOKEN: ${VIDXP_HTTP_STATIC_BEARER_TOKEN:-}
VIDXP_HTTP_OIDC_ISSUER: ${VIDXP_HTTP_OIDC_ISSUER:-}
VIDXP_HTTP_OIDC_AUDIENCE: ${VIDXP_HTTP_OIDC_AUDIENCE:-}
VIDXP_HTTP_OIDC_JWKS_URL: ${VIDXP_HTTP_OIDC_JWKS_URL:-}
VIDXP_HTTP_REQUIRED_SCOPES: ${VIDXP_HTTP_REQUIRED_SCOPES:-[]}
VIDXP_MCP_PUBLIC_URL: ${VIDXP_MCP_PUBLIC_URL:-}
VIDXP_MCP_MAX_RESOURCE_BYTES: ${VIDXP_MCP_MAX_RESOURCE_BYTES:-16777216}
VIDXP_HTTP_TRUSTED_HOSTS: '["127.0.0.1","localhost","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]'
VIDXP_MCP_ALLOWED_HOSTS: '["127.0.0.1:*","localhost:*","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]'
VIDXP_ARTIFACT_DOWNLOAD_PUBLIC_URL: https://${VIDXP_PUBLIC_API_HOST:?Set the public API host}/artifact-download
VIDXP_ARTIFACT_DOWNLOAD_SECRET: ${VIDXP_ARTIFACT_DOWNLOAD_SECRET:?Set a dedicated artifact-download secret of at least 32 characters}
VIDXP_ARTIFACT_DOWNLOAD_TTL_SECONDS: ${VIDXP_ARTIFACT_DOWNLOAD_TTL_SECONDS:-900}
VIDXP_UPLOAD_PUBLIC_ENDPOINT: ${VIDXP_UPLOAD_PUBLIC_ENDPOINT:?Set the public tusd endpoint ending in /uploads/}
VIDXP_UPLOAD_INTERNAL_ENDPOINT: http://tusd:8080/uploads/
VIDXP_UPLOAD_CLEANUP_TOKEN: ${VIDXP_UPLOAD_CLEANUP_TOKEN:?Set a cleanup token of at least 32 characters}
VIDXP_UPLOAD_HANDOFF_PUBLIC_URL: ${VIDXP_UPLOAD_HANDOFF_PUBLIC_URL:?Set the HTTPS VidXP URL ending in /upload-handoff}
VIDXP_UPLOAD_HANDOFF_SECRET: ${VIDXP_UPLOAD_HANDOFF_SECRET:?Set a dedicated handoff secret of at least 32 characters}
VIDXP_UPLOAD_CORS_ORIGIN_REGEX: ${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set a grouped exact-origin policy that permits the handoff origin}
VIDXP_UPLOAD_QUARANTINE_ROOT: /var/lib/vidxp/uploads
VIDXP_UPLOAD_MAX_BYTES: ${VIDXP_UPLOAD_MAX_BYTES:-53687091200}
VIDXP_UPLOAD_QUOTA_BYTES: ${VIDXP_UPLOAD_QUOTA_BYTES:-107374182400}
VIDXP_UPLOAD_SESSION_MAX_FILES: ${VIDXP_UPLOAD_SESSION_MAX_FILES:-10}
VIDXP_UPLOAD_SESSION_MAX_BYTES: ${VIDXP_UPLOAD_SESSION_MAX_BYTES:-107374182400}
VIDXP_UPLOAD_SESSION_TTL_SECONDS: ${VIDXP_UPLOAD_SESSION_TTL_SECONDS:-86400}

services:
postgres:
Expand Down Expand Up @@ -178,8 +193,9 @@ services:
- -hooks-http-size-limit=5120
- -hooks-http-retry=3
- -hooks-http-backoff=1s
- -cors-allow-origin=${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set an anchored browser-origin regex}
- -cors-allow-origin=${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set a grouped exact-origin browser policy}
- -cors-allow-headers=Authorization
- -cors-expose-headers=X-VidXP-Error
- -cors-max-age=600
- -network-timeout=60s
- -shutdown-timeout=30s
Expand Down
2 changes: 2 additions & 0 deletions desktop/runtime-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ pydeck==0.9.3
# via streamlit
pygments==2.20.0
# via rich
pyjwt==2.13.0
# via vidxp
pypika==0.51.1
# via chromadb
pyproject-hooks==1.2.0
Expand Down
2 changes: 1 addition & 1 deletion desktop/runtime-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"package_name": "vidxp",
"package_version": "0.4.0-b",
"dependency_index": "https://pypi.org/simple",
"dependency_constraints_sha256": "d3ed16906841952017f112903356bfe433a8515a37aa3f56e4af4dfa1f985835",
"dependency_constraints_sha256": "4225babc5298044494772e13c59a3346c706468069f0490025c988244e0b5bbb",
"python_version": "3.14.6",
"uv_version": "0.12.0",
"surfaces": {
Expand Down
Loading
Loading