[BUG] ext-apps viewer cannot fetch IIIF tiles despite correct CSP (regression ~Feb 26)

Resolved 💬 3 comments Opened Feb 27, 2026 by kintopp Closed Feb 28, 2026

Summary

When get_artwork_image is called on the rijksmuseum-mcp-plus MCP server, the MCP App viewer (ext-apps OpenSeadragon viewer) loads and renders its UI — header, metadata bar, zoom controls — but the image tiles never appear. The viewer canvas remains blank. This worked correctly until ~Feb 26; as of Feb 27 the IIIF tile requests from within the viewer iframe appear to be blocked.

Additionally, the viewerUrl returned in the tool response points to rijksmuseum-mcp-plus-production.up.railway.app, which is blocked by Claude's egress proxy (403 Forbidden, host_not_allowed). The model cannot programmatically verify or follow the link.

Last known working: ~2026-02-26
First observed failure: 2026-02-27

Expected Behaviour

The OpenSeadragon viewer should load IIIF tiles from iiif.micr.io and render the artwork image inside the ext-apps iframe, as it did until ~Feb 26. The MCP server declares both resourceDomains and connectDomains for https://iiif.micr.io in the viewer resource's CSP configuration (src/registration.ts L1527–1534):

csp: {
  resourceDomains: ["https://iiif.micr.io", "https://cdn.jsdelivr.net", "https://unpkg.com"],
  connectDomains: ["https://iiif.micr.io"],
},

Actual Behaviour

The viewer iframe loads. OpenSeadragon initialises (controls visible, navigator panel present). But the tile canvas is empty — no image content renders. The viewer behaves as if info.json or tile fetches to iiif.micr.io are being blocked within the iframe's network context.

Root Cause Analysis

Issue 1 — IIIF tile fetches blocked inside ext-apps iframe (primary, regression)
The OpenSeadragon viewer loads inside the ext-apps iframe but cannot fetch tiles from iiif.micr.io. The MCP server correctly declares iiif.micr.io in both resourceDomains (for img-src) and connectDomains (for connect-src) on the viewer resource. Despite this, tile requests appear to be blocked. This worked until ~Feb 26, suggesting a change in how Claude's client applies CSP or network restrictions to ext-apps iframes. The IIIF server itself is fully operational (info.json returns 200, CORS is access-control-allow-origin: *).

Issue 2 — Viewer domain not on egress allowlist (secondary)
The viewerUrl in the tool response points to rijksmuseum-mcp-plus-production.up.railway.app, which is blocked by Claude's egress proxy (403 Forbidden, host_not_allowed). The model cannot follow or verify this URL programmatically.

Full Tool Response

{
  "iiifInfoUrl": "https://iiif.micr.io/PJEZO/info.json",
  "width": 14645,
  "height": 12158,
  "viewerUrl": "https://rijksmuseum-mcp-plus-production.up.railway.app/viewer?iiif=PJEZO&title=The%20Night%20Watch%20...",
  "objectNumber": "SK-C-5",
  "title": "The Night Watch Militia Company of District II under the Command of Captain Frans Banninck Cocq",
  "creator": "Rembrandt van Rijn",
  "date": "1642",
  "license": "https://creativecommons.org/publicdomain/zero/1.0/",
  "physicalDimensions": "height 379.5 cm x width 453.5 cm x weight 337 kg x weight 170 kg",
  "collectionUrl": "https://www.rijksmuseum.nl/en/collection/SK-C-5"
}

Failure Trace

Step 1 — get_artwork_image tool call

Claude called the tool with objectNumber = "SK-C-5". The tool returned the payload above. No error was thrown.

Step 2 — web_fetch attempted on viewerUrl

PERMISSIONS_ERROR: "This URL cannot be fetched because it was not provided
by the user nor did it appear in any search/fetch results"

URLs returned by MCP tool calls are not trusted sources for web_fetch.

Step 3 — curl attempted on viewerUrl (via bash_tool)

HTTP/1.1 403 Forbidden
x-deny-reason: host_not_allowed

rijksmuseum-mcp-plus-production.up.railway.app is not on the egress allowlist.

Step 4 — curl attempted on iiifInfoUrl

https://iiif.micr.io/PJEZO/info.json succeeded (200 OK). iiif.micr.io is on the egress allowlist and returns valid IIIF Image API 3.0 (level2) data.

Reproduction

Tool:         get_artwork_image
objectNumber: SK-C-5
Expected:     OSD viewer renders with zoomable Night Watch image
Actual:       OSD viewer loads (controls visible) but tile canvas is blank

Reproducible on any objectNumber — the failure is structural, not specific to this artwork.

Environment

| | |
|---|---|
| Claude version | Sonnet 4.6 |
| Interface | claude.ai web chat |
| Date | 2026-02-27 |
| MCP server | rijksmuseum-mcp-plus-production.up.railway.app |
| IIIF endpoint | iiif.micr.io (accessible) |
| Egress proxy | Anthropic sandbox-egress-production |

Server-Side Investigation (no regression found)

  • No code changes to get_artwork_image or the viewer since Feb 21
  • The standalone viewer at /viewer?iiif=... returns correct HTML (200 OK, OSD configured properly)
  • IIIF server returns 200 with access-control-allow-origin: *
  • Railway deploy logs show all get_artwork_image calls succeeding (ok=true)
  • Current deployment (d935609a, Feb 26 18:02 UTC) is unchanged
  • The ImageInfoOutput schema includes both iiifInfoUrl and viewerUrl
  • CSP configuration (resourceDomains + connectDomains for iiif.micr.io) has not changed since the viewer was first deployed

Relevant Codebase Locations

| File | What | Lines |
|---|---|---|
| src/registration.ts | get_artwork_image tool handler | L892–949 |
| src/registration.ts | CSP config for viewer resource | L1527–1534 |
| apps/artwork-viewer/src/viewer.ts | MCP App (ext-apps) viewer | L1–555 |

---

Report generated by Claude Sonnet 4.6 tracing a live failed request. Investigation by Claude Opus 4.6.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗