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

@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: 3/5

[Medium impact] The change reaches people using VidXP through its MCP App.

Fix unreachable library paging and failed selection checks before merging.

Fix All in Claude CodeFindings

  1. P1 Later library pages are unreachable ▶
  2. P1 Unavailable videos appear selected ▶
  3. P2 Progress stops after one error ▶
Fix with agent prompt
### Issue 1
src/vidxp/assets/mcp_app/index.html:504
Every host entry point calls `render` without `mediaQuery`, but this condition requires it to show “Load more.” The only caller that supplies `mediaQuery` is the button's own handler, so users cannot reach videos beyond the first page. Capture the original `list_media` arguments before rendering and preserve its filters when following `next_cursor`.

### Issue 2
src/vidxp/assets/mcp_app/index.html:487-490
The widget saves `selectedMediaId` before `get_media` confirms that the video still exists, and it never checks `next.isError`. If a listed video becomes unavailable, a returned tool error still produces “Selected …” and leaves that unavailable ID in the model context. Check `next.isError` and save the selection only after `get_media` succeeds.

```suggestion
                  const next = await callTool("get_media", { media_id: item.media_id });
                  if (next.isError) throw new Error("VidXP could not select this video.");
                  const detail = next.structuredContent;
                  persistMediaSelection(item.media_id);
                  lede.textContent = `Selected ${detail.original_filename || item.original_filename || item.media_id}.`;
```

### Issue 3
src/vidxp/assets/mcp_app/index.html:564-568
The timer is cleared before `poll` runs, but a failed request never schedules another attempt. A returned `isError` also reaches `render(next)`, which replaces the status view and removes its refresh button. Keep the last status on errors, check `next.isError`, and schedule a bounded retry while the same job view remains active.

---

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 views to the MCP App, and attaches the app to list_media and get_job_status.

  • The MCP App lets people choose videos from the registered library.
  • The MCP App shows job progress and keeps active status up to date.

Haseeb-1698 explicitly acknowledged that the widget was not exercised in a real MCP host or against a live indexing job; that validation gap is not reported as a finding.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Host tool result] --> B{Result shape}
  B --> C[Existing upload or evidence view]
  B --> D[Library view]
  B --> E[Job progress view]
  D --> F[Select video]
  F --> G[Save selectedMediaId]
  G --> H[Call get_media]
  E --> I[Wait for timer or manual refresh]
  I --> J[Call get_job_status]
  J --> B
Loading

Reviews (1) · Last reviewed commit: "fix(mcp): address MCP App review finding..." · Reviewed by Greptile

wrapper.append(listing);
}

if (typeof data.next_cursor === "string" && data.next_cursor && query) {

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 Later library pages are unreachable

Every host entry point calls render without mediaQuery, but this condition requires it to show “Load more.” The only caller that supplies mediaQuery is the button's own handler, so users cannot reach videos beyond the first page. Capture the original list_media arguments before rendering and preserve its filters when following next_cursor.

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

Comment:
**Later library pages are unreachable**

Every host entry point calls `render` without `mediaQuery`, but this condition requires it to show “Load more.” The only caller that supplies `mediaQuery` is the button's own handler, so users cannot reach videos beyond the first page. Capture the original `list_media` arguments before rendering and preserve its filters when following `next_cursor`.

---

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

Comment on lines +487 to +490
persistMediaSelection(item.media_id);
const next = await callTool("get_media", { media_id: item.media_id });
const detail = next.structuredContent;
lede.textContent = `Selected ${detail.original_filename || item.original_filename || 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 Unavailable videos appear selected

The widget saves selectedMediaId before get_media confirms that the video still exists, and it never checks next.isError. If a listed video becomes unavailable, a returned tool error still produces “Selected …” and leaves that unavailable ID in the model context. Check next.isError and save the selection only after get_media succeeds.

Suggested change
persistMediaSelection(item.media_id);
const next = await callTool("get_media", { media_id: item.media_id });
const detail = next.structuredContent;
lede.textContent = `Selected ${detail.original_filename || item.original_filename || item.media_id}.`;
const next = await callTool("get_media", { media_id: item.media_id });
if (next.isError) throw new Error("VidXP could not select this video.");
const detail = next.structuredContent;
persistMediaSelection(item.media_id);
lede.textContent = `Selected ${detail.original_filename || item.original_filename || item.media_id}.`;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/vidxp/assets/mcp_app/index.html
Line: 487-490

Comment:
**Unavailable videos appear selected**

The widget saves `selectedMediaId` before `get_media` confirms that the video still exists, and it never checks `next.isError`. If a listed video becomes unavailable, a returned tool error still produces “Selected …” and leaves that unavailable ID in the model context. Check `next.isError` and save the selection only after `get_media` succeeds.

```suggestion
                  const next = await callTool("get_media", { media_id: item.media_id });
                  if (next.isError) throw new Error("VidXP could not select this video.");
                  const detail = next.structuredContent;
                  persistMediaSelection(item.media_id);
                  lede.textContent = `Selected ${detail.original_filename || item.original_filename || item.media_id}.`;
```

---

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

Comment on lines +564 to +568
const next = await callTool("get_job_status", { job_id: jobId });
if (generation !== progressGeneration) return;
render(next);
} catch (_error) {
if (generation === progressGeneration) notice.textContent = "Could not refresh job status.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Progress stops after one error

The timer is cleared before poll runs, but a failed request never schedules another attempt. A returned isError also reaches render(next), which replaces the status view and removes its refresh button. Keep the last status on errors, check next.isError, and schedule a bounded retry while the same job view remains active.

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

Comment:
**Progress stops after one error**

The timer is cleared before `poll` runs, but a failed request never schedules another attempt. A returned `isError` also reaches `render(next)`, which replaces the status view and removes its refresh button. Keep the last status on errors, check `next.isError`, and schedule a bounded retry while the same job view remains active.

---

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

This branch has not been deployed

No deployments
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