Skip to content

feat(mcp): select library media and follow indexing progress in the MCP App - #152

Open
Haseeb-1698 wants to merge 2 commits into
grayhatdevelopers:mainfrom
Haseeb-1698:feat/73-mcp-app-library-and-progress
Open

Haseeb-1698 wants to merge 2 commits into
grayhatdevelopers:mainfrom
Haseeb-1698:feat/73-mcp-app-library-and-progress

Conversation

@Haseeb-1698

@Haseeb-1698 Haseeb-1698 commented Sep 6, 2026

Copy link
Copy Markdown

Related issue

Closes #73.

Summary

The MCP App already covered two of the four items in the issue's "first useful version": uploading (create_media_upload) and inspecting evidence boards, keyframes and clips (present_job_evidence). This adds the other two , selecting a video that is already registered, and following indexing progress.

Both are driven by tools that already exist, so there is no new tool and no separate application backend:

  • list_media now carries the app template. The widget lists registered videos with state, duration, size and container, pages through next_cursor, and on selection writes selectedMediaId via ui/update-model-context and setWidgetState, then confirms with get_media.
  • get_job_status now carries it too. The widget renders stage, message and current/total with a progress bar, offers a manual refresh, and re-polls itself using the job's own poll_after_seconds while terminal is false, clearing its timer on every re-render.

I used get_job_status rather than get_job because its own description recommends it for active work and JobSummary is the compact record.

Affected interfaces: two read-only tools gain _meta only; no signatures, arguments or return types change. The widget additions are new render paths dispatched after the existing upload and evidence branches, so current results render exactly as before. No compatibility or migration impact.

Validation

Run on linux/amd64, Python 3.12, at 24b916a, after uv sync --all-extras:

Command Result
ruff check . (0.16.6, whole repo) All checks passed
uv run --no-sync pytest -q tests/test_mcp.py 49 passed, 1 skipped, 7 subtests passed (54.98s)
PYTHONPATH=. uv run --no-sync pytest -q (full suite) 723 passed, 7 failed, 10 skipped, 106 subtests (4m11s)
node --check on the widget's extracted <script> passed

The 7 failures are chromadb RuntimeErrors from chromadb/config.py:375 in test_benchmarks.py, test_local_snapshots.py and test_storage_integration.py. They reproduce identically with this branch's two files stashed (7 failed, 32 passed on the clean tree), so they are pre-existing on this machine and unrelated to the change. The full suite also needs PYTHONPATH=. here or five packaging modules fail to import utils; that is likewise unchanged by this branch.

Not exercised: I have not run the widget inside a real MCP host, so the new views are verified by unit tests, lint and JavaScript syntax only, not against an actual host's ui/* message flow, and not against a live indexing job. Please treat the rendering as unproven at that boundary.

I opened this rather than waiting on my comment in #73 so there is something concrete to react to. Happy to rework the scope, split it into library-selection and progress separately, or drop it if you have a different direction in mind.

@SaadBazaz

Copy link
Copy Markdown
Member

Hey @Haseeb-1698,
Thanks for the Pull Request.
As a starter, we require all contributors to "star" and "fork" the repo, in order to determine if there really is a human behind the wheel or an autonomous agent.
Please star the repo for our review to proceed.
Thanks!

@Haseeb-1698

Haseeb-1698 commented Sep 9, 2026

Copy link
Copy Markdown
Author

Hi @SaadBazaz, both are done: the repo is starred, and this PR comes from my fork (Haseeb-1698/vidxp, created 6 Sep).

For context on the human question: I came to VidXP through the FAST-NUCES Islamabad Career Services invitation, and I commented on #73 first to check the direction before writing anything. Happy to answer anything about the change , why get_job_status over get_job, why the polling is driven by the job's own poll_after_seconds, or why I kept it to _meta on two existing tools rather than adding any.

I flagged one limit in the PR body and it still stands: I have not run the widget inside a real MCP host, so the two new views are covered by unit tests, lint and JS syntax only, not against a live ui/* message flow. If you have a host setup you'd like it checked against before review, point me at it and I'll run it.

@tulayha tulayha left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful extension of the existing MCP App and tools. Please address the four inline UI findings and add focused coverage for these cases. The requested changes stay within the current library-selection and progress scope.

Comment thread src/vidxp/assets/mcp_app/index.html Outdated
more.disabled = true;
notice.textContent = "";
try {
render(await callTool("list_media", { cursor: data.next_cursor }));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve the original filename and state filters when loading another page. Media cursors are tied to those filters, but this call sends only the cursor. For a filtered list, clicking Load more therefore fails with an invalid media cursor instead of showing the next page.

Comment thread src/vidxp/assets/mcp_app/index.html Outdated
notice.textContent = latestResult.isError ? "VidXP returned an error." : "";
if (data.view === "upload" || data.upload_session_url || data.aggregate_state) renderUpload(data);
else if (data.view === "evidence" || data.board) renderEvidence(data, latestResult);
else if (data.view === "library" || asArray(data.items).some((item) => typeof asObject(item).media_id === "string")) renderLibrary(data);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty list_media result has items=[] and no view field, so it never reaches renderLibrary and instead displays "This tool result does not include an interactive view." Please recognize empty media pages too, so a new library or a filter with no matches gets an appropriate empty state.

Comment thread src/vidxp/assets/mcp_app/index.html Outdated
};

const renderProgress = (data) => {
if (progressTimer !== null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The polling timer is cleared only when rendering another progress view. If a library or evidence result replaces this view, the old timer still runs and its response can replace the current screen with the previous job. Please stop polling when leaving the view or tearing down the widget, and ignore responses from requests belonging to a view that is no longer active.

const actions = element("div", "toolbar");
const refresh = element("button", "button", "Refresh status");
refresh.type = "button";
refresh.addEventListener("click", async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the refresh request fails, poll() catches the error but this button stays disabled. The user cannot retry that refresh. Please restore the button after a failed request, for example through finally, and cover the failure-and-retry path.

…CP App

The MCP App covered uploading and inspecting evidence. Two items from the
issue's first useful version were missing: selecting a video that is
already registered, and following indexing progress.

Attach the existing app template to list_media and get_job_status, and add
the two matching views to the widget. list_media pages through the library
and records the chosen media in the model context; get_job_status renders
stage, message and step counts, and re-polls itself using the job's own
poll_after_seconds until the job is terminal.

No new tools and no separate application backend: both views drive tools
that already exist, and the reused media and job lifecycle stays the
source of truth.

Closes grayhatdevelopers#73.
Four corrections from review, plus behavioural coverage for each.

Load more sent only the cursor, so a filtered page failed with an invalid
media cursor. The widget now carries the query it issued and merges the
cursor into it. MediaPage returns items, total and next_cursor only, so an
agent-issued first page leaves no recoverable filter context; pagination is
withheld there rather than sending a cursor that cannot honour the filters.

An empty media page has items: [] and no view field, so the dispatcher fell
through to the no-interactive-view message. It now recognises a MediaPage by
shape. The integer total is what separates it from JobPage, which carries
items and next_cursor but no total.

Progress polling cleared its timer only when another progress view rendered,
so a library or evidence result left it running and a late response could
replace the current screen. A generation token is now taken when the view
renders and rechecked before any response touches the DOM; every render and
the resource-teardown handler stop polling.

A failed manual refresh left its button disabled with no way to retry. The
button is now restored in a finally block.

Add web/mcp-app with node --test coverage driving the shipped widget in a
DOM stub: six cases, one per behaviour. They fail against the pre-fix widget
and pass after.
@Haseeb-1698
Haseeb-1698 force-pushed the feat/73-mcp-app-library-and-progress branch from 3590566 to fcb1947 Compare September 14, 2026 19:02
@Haseeb-1698

Haseeb-1698 commented Sep 14, 2026

Copy link
Copy Markdown
Author

Thanks for the review @tulayha. All four are addressed, and the branch is rebased onto main.

1. Load more dropping the filters. The widget now carries the query it issued and merges the cursor into it, so a filtered page continues correctly.

There is a half of this I could not fix inside the widget, and I would rather flag it than paper over it. MediaPage returns items, total and next_cursor only. It does not echo filename or state back. For a page the widget requested itself it knows the arguments it sent, so filters carry forward. For the first page, issued by the agent rather than the widget, there is no recoverable filter context. Rather than send a cursor that cannot honour the filters, the widget now withholds pagination in that case.

That is a deliberate trade and it has a visible cost: on an agent-issued first page, Load more no longer appears at all. Two ways out if you would rather it did. Either MediaPage echoes the applied filters back, which is a server change and outside this PR's scope, or it stays as is. Your call, and happy to do either.

2. Empty media page. The dispatcher now recognises a MediaPage by shape rather than by finding a media_id in a non-empty list, so an empty library or a filter with no matches reaches the empty state. The discriminator requires an integer total, which is what separates it from JobPage, since that carries items and next_cursor but no total. So list_jobs results are not swallowed.

3. Polling timer and stale responses. A generation token is taken when the progress view renders and rechecked before any response is allowed to touch the DOM, so an in-flight get_job_status from a view that has been replaced is discarded rather than overwriting the current screen. Every render stops polling, and so does the ui/resource-teardown handler.

4. Failed refresh. The button is restored in a finally, so a failed request can be retried.

Validation

New web/mcp-app package with node --test coverage, one case per finding. It drives the shipped index.html in a DOM stub rather than a copy, so it cannot drift from the asset:

node --test web/mcp-app/test/widget-behavior.test.mjs
Widget under test Result
Pre-fix 0 pass, 6 fail
This branch 6 pass, 0 fail

Python suite, on a clean main checkout and again with this change:

uv run --no-sync ruff check .          # All checks passed (both trees)
PYTHONPATH=. uv run --no-sync pytest -q
Tree Result
Clean 735 passed, 2 failed, 3 skipped, 106 subtests in 407.45s
Patched 735 passed, 2 failed, 3 skipped, 106 subtests in 397.21s

The same two tests fail on both trees: test_native_ingestion.py::test_local_ingestion_automatically_indexes_and_becomes_searchable and ::test_streamable_http_browser_upload_indexes_in_one_session, both reporting Autonomous indexing did not finish without status polling on a 2-core box. Pre-existing and timing-dependent, with 0 new failures.

Still true from the original description: I have not run this inside a real MCP host, so the ui/* message flow is exercised against a stub rather than a live host.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a portable MCP App interface

3 participants