Skip to content

Fix resource refresh button not triggering re-fetch#1148

Open
olaservo wants to merge 16 commits intomodelcontextprotocol:mainfrom
olaservo:feature/fix-resource-refresh-button-1120
Open

Fix resource refresh button not triggering re-fetch#1148
olaservo wants to merge 16 commits intomodelcontextprotocol:mainfrom
olaservo:feature/fix-resource-refresh-button-1120

Conversation

@olaservo
Copy link
Copy Markdown
Member

@olaservo olaservo commented Mar 15, 2026

Summary

Fixes #1120

  • The readResource function had an early return when the resource URI was already in resourceContentMap, which prevented the Refresh button from ever re-fetching
  • Added a { bypassCache } options parameter to readResource to skip the cache when the Refresh button is clicked
  • When not bypassing cache, cached content is now properly synced to the display (fixes switching between previously viewed resources showing stale content)
  • Errors from failed resource reads are no longer cached, so re-clicking a resource will retry instead of serving a stale error

Changes

  • client/src/App.tsx — Added { bypassCache } options parameter to readResource; stopped caching errors in resourceContentMap; forwarded options through the prop wrapper
  • client/src/components/ResourcesTab.tsx — Updated type signature; Refresh button now passes { bypassCache: true }

Test plan

  • Existing unit tests pass
  • TypeScript type-check passes (npx tsc --noEmit)
  • Build passes (npm run build-client)
  • Lint passes (npm run lint)
  • Manual verification: connect to an MCP server, click a resource, click Refresh → new request appears in history
  • Manual verification: click resource A, click resource B, click resource A again → correct cached content displays
  • Manual verification: simulate a failed resource read, then re-click → resource is retried instead of showing stale error

🦉 Generated with Claude Code

…ocol#1120)

The readResource function had an early return when the resource URI
was already cached, preventing the Refresh button from re-fetching.
Add a force parameter to bypass the cache on refresh, and update the
cache-hit path to still sync the displayed content when switching
between previously viewed resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@olaservo olaservo force-pushed the feature/fix-resource-refresh-button-1120 branch from 234b209 to 70a6b03 Compare April 12, 2026 21:46
@olaservo olaservo marked this pull request as ready for review April 12, 2026 21:48
Failed resource reads were cached in resourceContentMap, causing stale
errors to be served on re-click instead of retrying. Errors now only
set the display content without polluting the cache.

Also renamed the `force` boolean parameter to a `{ bypassCache }` options
object for self-documenting call sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@olaservo olaservo force-pushed the feature/fix-resource-refresh-button-1120 branch from c3cd4ef to 3054236 Compare April 12, 2026 22:04
olaservo and others added 4 commits April 12, 2026 19:30
The test depends on npx downloading @modelcontextprotocol/server-everything
at runtime and uses waitForTimeout, making it unreliable in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ions

Clear cached resource content when a bypassCache refresh fails, so
subsequent clicks retry the fetch instead of serving stale data.
Rename the inline wrapper parameter from opts to options for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes resource re-fetch behavior in the client by adding a bypassCache option to readResource, ensuring the Refresh button triggers a new server request and cached content is correctly synced to the UI when revisiting previously viewed resources.

Changes:

  • Add { bypassCache } option to readResource and use it to bypass cache on Refresh.
  • Sync cached resource content to the display instead of early-returning without updating UI state.
  • Stop caching read errors in resourceContentMap (intended to ensure retries on re-click).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
client/src/App.tsx Adds bypassCache support to readResource, updates caching/refresh logic, and forwards options through the ResourcesTab prop wrapper.
client/src/components/ResourcesTab.tsx Updates readResource prop signature and passes { bypassCache: true } from the Refresh button.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx Outdated
Comment thread client/src/components/ResourcesTab.tsx
Previously, failed resource_link reads left resourceContentMap[uri]
empty, so ResourceLinkView rendered an empty expansion with no error
feedback when a read failed in the Tools tab. Track errors in a
separate resourceErrorMap and thread it through ToolsTab/ToolResults
to ResourceLinkView so the user sees the error message. The existing
cache-skip behavior is preserved (errors don't pollute the content
map, so retries still work).

Also adds a ResourcesTab test asserting the Refresh button passes
{ bypassCache: true } to readResource, to prevent regression of modelcontextprotocol#1120.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx Outdated
Comment thread client/src/App.tsx Outdated
…r JSON to resourceContent

Don't delete resourceContentMap[uri] when a bypassCache refresh fails;
a transient network error should not wipe the last-known-good content.
To prevent serving stale content after a known error, the cache-hit
guard now also skips cache when resourceErrorMap[uri] is set, forcing
a re-fetch until a successful read clears the error.

Also stop writing JSON.stringify({ error }) into resourceContent in
the catch block. sendMCPRequest already sets errors.resources (which
the ResourcesTab Alert displays), and the Tools tab shows errors via
resourceErrorMap. Writing error JSON into resourceContent was both
redundant and could bleed through as JsonView content after callers
cleared errors.resources before the next read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx Outdated
Comment thread client/src/components/ToolResults.tsx Outdated
Comment thread client/src/components/ResourceLinkView.tsx
Switch from truthiness checks to presence checks in the resource
error/content handling so an empty-string error message is still
treated as an error. readResource's cache-hit guard now uses
`uri in resourceContentMap` / `!(uri in resourceErrorMap)`,
ToolResults passes `resourceError?.[item.uri]` (possibly undefined)
through to ResourceLinkView, and ResourceLinkView renders the error
branch when `resourceError !== undefined`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx Outdated
Comment thread client/src/App.tsx Outdated
olaservo and others added 2 commits April 14, 2026 07:08
The `in` operator also matches inherited properties like `toString`
and `constructor`, which would cause false cache hits if a server ever
returns a URI with one of those names. Switch the cache-hit guard and
the error-map cleanup to Object.prototype.hasOwnProperty.call so only
real cached entries match.

Object.hasOwn would be cleaner but requires ES2022 lib support which
the current tsconfig doesn't include.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx
setResourceContent(resourceContentMap[uri]);
return;
}

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Because resourceErrorMap[uri] is only cleared on success, any retry attempt will continue to have an error entry until the request completes. Since some UI paths render resourceError in preference to cached content, this can leave a stale error visible while the retry is in-flight. Consider clearing resourceErrorMap[uri] when starting a new read (after passing the cache check / before sending the request), not only when a read succeeds.

Suggested change
setResourceErrorMap((prev) => {
if (!hasOwn.call(prev, uri)) return prev;
const next = { ...prev };
delete next[uri];
return next;
});

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 01ea4cb — moved resourceErrorMap[uri] clearing from the success handler to right after the cache-hit check (before sending the request). During an in-flight retry, the UI now shows last-known-good content instead of the stale error.

olaservo and others added 3 commits April 14, 2026 08:12
Previously, resourceErrorMap[uri] was only cleared when a read
succeeded. During an in-flight retry (e.g., after clicking Refresh on
a previously-failed resource), the stale error would remain in the
map and render in ResourceLinkView while the retry was still pending.

Clear the error entry after the cache-hit check passes, right before
sending the request, so the UI shows the last-known-good content (or
a loading state) during the retry instead of the prior error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx
Comment on lines 957 to 962
console.error(`[App] Failed to read resource ${uri}:`, error);
const errorString = (error as Error).message ?? String(error);
setResourceContentMap((prev) => ({
setResourceErrorMap((prev) => ({
...prev,
[uri]: JSON.stringify({ error: errorString }),
[uri]: errorString,
}));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

errorString uses nullish coalescing (??), so an Error with an empty message (e.g. new Error("")) will produce an empty string and render a blank error. Prefer a fallback that treats empty messages as missing (e.g. use || and/or include error.name).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in f879c0d — switched from ?? to || so an empty .message falls through to String(error), which always produces a non-empty representation.

The ?? operator only falls back on null/undefined, so an Error with
an empty message (new Error("")) would produce an empty string and
render a blank error block. Switch to || so empty strings also fall
through to String(error), which produces a non-empty representation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx
Comment on lines +908 to +914
const readResource = async (
uri: string,
{ bypassCache = false }: { bypassCache?: boolean } = {},
) => {
if (fetchingResources.has(uri)) {
return;
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

fetchingResources is React state, so fetchingResources.has(uri) can be stale if readResource is triggered twice before the state update is applied (e.g., double-click Refresh / rapid re-select). That can lead to duplicate resources/read requests for the same URI. Consider tracking in-flight URIs in a useRef (or similar) so the guard is synchronous and cannot race with state updates.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid observation, but this is a pre-existing issue — fetchingResources was already React state before this PR. The race condition on rapid clicks applies to the original code as well. A useRef-based guard would be a good improvement, but it's out of scope for this bug fix and should be addressed in a separate PR.

Comment thread client/src/App.tsx
Comment on lines +908 to +912
const readResource = async (
uri: string,
{ bypassCache = false }: { bypassCache?: boolean } = {},
) => {
if (fetchingResources.has(uri)) {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The new cache-bypass and error-not-cached behavior in readResource is central to fixing #1120, but it’s currently only covered indirectly via ResourcesTab prop tests (it doesn’t assert that App actually re-fetches when cached, nor that errors aren’t cached). Consider adding an App-level unit/integration test that verifies: (1) cache hits sync resourceContent without calling makeRequest, (2) { bypassCache: true } forces a re-fetch even when cached, and (3) failed reads don’t populate resourceContentMap so a subsequent click retries.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid suggestion. The ResourcesTab prop tests verify that the correct arguments (bypassCache: true for Refresh, no options for normal clicks) are passed, but App-level integration tests verifying the actual cache/retry behavior would add more confidence. This would require mocking sendMCPRequest and the full App rendering context — better suited as a follow-up rather than this bug fix PR.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/App.tsx Outdated
hasOwn.call(resourceContentMap, uri) &&
!hasOwn.call(resourceErrorMap, uri)
) {
setResourceContent(resourceContentMap[uri]);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

readResource now calls setResourceContent(resourceContentMap[uri]) on a cache hit. When readResource is triggered from non-Resources contexts (e.g. Tools tab expanding a resource_link), this will update the global resourceContent string without updating selectedResource, so the Resources tab can later display content that doesn’t match the selected resource header. Consider only syncing resourceContent on cache hits when the current tab is resources (or when selectedResource?.uri === uri), and otherwise just return without touching resourceContent.

Suggested change
setResourceContent(resourceContentMap[uri]);
if (currentTabRef.current === "resources") {
setResourceContent(resourceContentMap[uri]);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in fca508c — guarded the cache-hit setResourceContent call with currentTabRef.current === "resources" so expanding a resource_link in the Tools tab no longer overwrites the Resources tab's displayed content.

The cache-hit path called setResourceContent unconditionally, which
meant expanding a resource_link in the Tools tab could overwrite the
Resources tab's displayed content without updating selectedResource.
Guard the setResourceContent call with a currentTabRef check so only
cache hits from the Resources tab update the display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@cliffhall cliffhall added the v1 label Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resource refresh button is not working

3 participants