Skip to content

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

Merged
tulayha merged 7 commits into
grayhatdevelopers:mainfrom
Haseeb-1698:feat/73-mcp-app-library-and-progress
Oct 26, 2026
Merged

tulayha merged 7 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
Contributor

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
Contributor

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
Contributor 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
Contributor 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.

@tulayha

tulayha commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

@greptileai

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Heads up: Greptile reviewed this. Push back if it's wrong — it has no feelings.

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect two workflows inside the MCP App.

Fix outdated library replies before merging so they cannot undo a newer video choice or replace another view.

Fix All in Claude CodeFindings

  1. P1 Older replies undo newer choices ▶
Fix with agent prompt
### Issue 1
src/vidxp/assets/mcp_app/index.html:488-490
Each selection button disables only itself. A user can choose video A and then B while both `get_media` calls are pending. If A finishes last, `persistMediaSelection` replaces B with A.

The “Load more” handler has the same problem: its reply calls `render` even after a newer host result opens another view. That can replace job progress and stop its updates.

Ignore replies from an older view, and let only the latest selection request save `selectedMediaId`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds library selection and job progress to the MCP App.

  • People can browse registered videos and choose one to work with.
  • People can follow job progress in the MCP App.

Diagram

sequenceDiagram
  participant User
  participant App
  participant Host
  participant VidXP
  Host->>App: Library result and tool input
  User->>App: Choose a video
  App->>Host: get_media(media_id)
  Host->>VidXP: Read media
  VidXP-->>App: Media details
  App->>Host: Save selectedMediaId
  Host->>App: Job status result
  loop While job is active
    App->>Host: get_job_status(job_id)
    Host->>VidXP: Read status
    VidXP-->>App: Updated progress
  end
Loading

Reviews (2) · Last reviewed commit: "Merge branch 'main' into feat/73-mcp-app..." · Reviewed by Greptile

Comment thread src/vidxp/assets/mcp_app/index.html Resolved
Comment thread src/vidxp/assets/mcp_app/index.html Resolved
Comment thread src/vidxp/assets/mcp_app/index.html Resolved
Keep the arguments from ui/notifications/tool-input (or ChatGPT's toolInput) and use them as the first page's query, so Load more appears and keeps the original filters. Each loaded page is appended to the list shown.
A failed or error get_job_status check now keeps the progress view and its Refresh button on screen, says so in the notice, and schedules the next check after the same poll_after_seconds delay.
Call get_media first, treat an error result as a failure like the upload and evidence paths do, and only then save the selection to the model context and widget state.
Run npm test in web/mcp-app next to the upload-page checks. The package has no dependencies, so it needs no npm ci. Internal-only.

@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.

Fixed the four remaining gaps on our side so this can merge. Notes inline on each; every fix comes with a test that fails on your last commit.

return;
}
if (message.method === "ui/notifications/tool-result") render(message.params);
if (message.method === "ui/notifications/tool-input") toolInput = asObject(message.params?.arguments);

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.

Load more never showed on the first page: it only appears when the widget knows the original query, and the first page comes from the host without one. The host already sends the tool arguments in ui/notifications/tool-input before the result (window.openai.toolInput in ChatGPT), so we keep them here and pass them to the first render. Load more also adds pages to the list now instead of replacing it. (659b88b)


const jobId = typeof data.job_id === "string" ? data.job_id : "";
if (jobId) {
const schedule = () => {

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.

One failed status check used to end polling: the catch set the notice but never scheduled the next check, and an error result replaced the progress view and its Refresh button. Every path through poll reschedules now, and an error only updates the notice. (5fd0874)

notice.textContent = "";
try {
const next = await callTool("get_media", { media_id: item.media_id });
if (next.isError) throw new Error("VidXP could not find that video.");

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 selection was saved before get_media answered, and an error reply counted as success, so a missing video still showed as selected. It gets the same isError check as the upload and evidence buttons, and is saved only after that passes. (99546a2)

Comment thread .github/workflows/ci.yml
run: npm run check:bundle
working-directory: web/upload-page

- name: Test MCP App widget

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.

Nothing ran web/mcp-app before this, so the new tests could break without anyone noticing. A new test folder needs its CI step in the same PR. (9a44c96)

@Haseeb-1698

Copy link
Copy Markdown
Contributor Author

Thanks @tulayha, glad to see these fixes! The tool-input handling for Load more, the polling that keeps going after a failed check, and saving the selection only after get_media confirms it all look good. Nice to have the CI step for web/mcp-app in too.

Are you planning to merge this, or is there anything you need from me first?

Comment on lines +488 to +490
const next = await callTool("get_media", { media_id: item.media_id });
if (next.isError) throw new Error("VidXP could not find that video.");
persistMediaSelection(item.media_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Older replies undo newer choices

Each selection button disables only itself. A user can choose video A and then B while both get_media calls are pending. If A finishes last, persistMediaSelection replaces B with A.

The “Load more” handler has the same problem: its reply calls render even after a newer host result opens another view. That can replace job progress and stop its updates.

Ignore replies from an older view, and let only the latest selection request save selectedMediaId.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/vidxp/assets/mcp_app/index.html
Line: 488-490

Comment:
**Older replies undo newer choices**

Each selection button disables only itself. A user can choose video A and then B while both `get_media` calls are pending. If A finishes last, `persistMediaSelection` replaces B with A.

The “Load more” handler has the same problem: its reply calls `render` even after a newer host result opens another view. That can replace job progress and stop its updates.

Ignore replies from an older view, and let only the latest selection request save `selectedMediaId`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

@tulayha
tulayha merged commit c601039 into grayhatdevelopers:main Oct 26, 2026
15 checks passed
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