🐞 Bug Summary
With MCPGATEWAY_DIRECT_PROXY_ENABLED=true and a gateway in gateway_mode=direct_proxy, a resources/read from a normal MCP client returns empty content. The direct_proxy branch successfully fetches the resource from the upstream and builds a TextResourceContents/BlobResourceContents, then the shared post-fetch block calls getattr(content, "id") on that object — which has no id field — and raises AttributeError. The exception is caught at the transport layer and turned into empty content, so the client never sees the fetched data.
This affects the path a normal client takes (no X-Context-Forge-Gateway-Id header). With that header set, the request uses a different transport-level proxy path that returns the content correctly.
🧩 Affected Component
🔁 Steps to Reproduce
- Set
MCPGATEWAY_DIRECT_PROXY_ENABLED=true and restart the gateway.
- Federate an upstream MCP server that exposes a resource, then set the gateway to direct_proxy:
PUT /gateways/{id} with {"gateway_mode":"direct_proxy"} (the gateway also needs transport: STREAMABLEHTTP).
- From an MCP client,
resources/read the federated resource through /servers/{id}/mcp — without sending the X-Context-Forge-Gateway-Id header.
Reproduced locally against a FastMCP upstream federated into a local gateway (sqlite), for both a text and a binary (image/png) resource.
🤔 Expected Behavior
The read returns the upstream resource's content (text or blob), fetched live. Instead the client receives empty content:
ReadResourceResult(contents=[TextResourceContents(uri=AnyUrl('test://schema'), mimeType='text/plain', text='')])
📓 Logs / Error Output
ERROR mcpgateway.transports.streamablehttp_transport - Error reading resource 'test://schema': 'TextResourceContents' object has no attribute 'id'
File ".../mcpgateway/services/resource_service.py", line 2549, in read_resource
resource_id=getattr(content, "id"),
AttributeError: 'TextResourceContents' object has no attribute 'id'
The binary resource fails identically with 'BlobResourceContents' object has no attribute 'id' at the same line.
🧠 Environment Info
| Key |
Value |
| Version or commit |
v1.0.4 (also present on main) |
| Runtime |
Python 3.12.13 |
| Platform / OS |
Linux / macOS (reproduced locally, sqlite) |
| Container |
mcp-contextforge-gateway==1.0.4 |
| Flags |
MCPGATEWAY_DIRECT_PROXY_ENABLED=true, gateway gateway_mode=direct_proxy |
🧩 Additional Context
The direct_proxy branch in resource_service.py::read_resource (around lines 2381–2432, gated on gateway.gateway_mode == "direct_proxy" and settings.mcpgateway_direct_proxy_enabled) fetches over streamablehttp_client and constructs a TextResourceContents or BlobResourceContents from mcpgateway/common/models.py. Those classes define only uri, mime_type, meta, and text/blob — there is no id field.
Because both subclass ResourceContents, execution then falls into the post-fetch "RESOLVE CONTENT" block (around lines 2541–2576). Its first branch, if isinstance(content, (ResourceContent, ResourceContents, TextContent)): (line 2545), matches, and the next statement runs resource_id=getattr(content, "id") (line 2549) with no default, raising AttributeError. There is no early return between the direct_proxy fetch and that call, so every direct_proxy text or blob read hits it. The handler in streamablehttp_transport.py catches Exception and returns "".
The test suite works around this by monkey-patching id-bearing subclasses over the real models (tests/unit/mcpgateway/services/test_resource_service.py ~7127), with a comment noting the post-direct-proxy code calls getattr(content, "id") unconditionally — so the production classes that lack id take a path the tests never exercise.
A fix would return the constructed TextResourceContents/BlobResourceContents from the direct_proxy branch directly rather than routing it through the legacy getattr(content, "id") post-processing, or guard that block with getattr(content, "id", None) plus a blob-aware path. A test asserting that a direct_proxy text read and an image/png read both return content end-to-end would cover it.
🐞 Bug Summary
With
MCPGATEWAY_DIRECT_PROXY_ENABLED=trueand a gateway ingateway_mode=direct_proxy, aresources/readfrom a normal MCP client returns empty content. The direct_proxy branch successfully fetches the resource from the upstream and builds aTextResourceContents/BlobResourceContents, then the shared post-fetch block callsgetattr(content, "id")on that object — which has noidfield — and raisesAttributeError. The exception is caught at the transport layer and turned into empty content, so the client never sees the fetched data.This affects the path a normal client takes (no
X-Context-Forge-Gateway-Idheader). With that header set, the request uses a different transport-level proxy path that returns the content correctly.🧩 Affected Component
🔁 Steps to Reproduce
MCPGATEWAY_DIRECT_PROXY_ENABLED=trueand restart the gateway.PUT /gateways/{id}with{"gateway_mode":"direct_proxy"}(the gateway also needstransport: STREAMABLEHTTP).resources/readthe federated resource through/servers/{id}/mcp— without sending theX-Context-Forge-Gateway-Idheader.Reproduced locally against a FastMCP upstream federated into a local gateway (sqlite), for both a text and a binary (
image/png) resource.🤔 Expected Behavior
The read returns the upstream resource's content (text or blob), fetched live. Instead the client receives empty content:
📓 Logs / Error Output
The binary resource fails identically with
'BlobResourceContents' object has no attribute 'id'at the same line.🧠 Environment Info
v1.0.4(also present onmain)mcp-contextforge-gateway==1.0.4MCPGATEWAY_DIRECT_PROXY_ENABLED=true, gatewaygateway_mode=direct_proxy🧩 Additional Context
The direct_proxy branch in
resource_service.py::read_resource(around lines 2381–2432, gated ongateway.gateway_mode == "direct_proxy"andsettings.mcpgateway_direct_proxy_enabled) fetches overstreamablehttp_clientand constructs aTextResourceContentsorBlobResourceContentsfrommcpgateway/common/models.py. Those classes define onlyuri,mime_type,meta, andtext/blob— there is noidfield.Because both subclass
ResourceContents, execution then falls into the post-fetch "RESOLVE CONTENT" block (around lines 2541–2576). Its first branch,if isinstance(content, (ResourceContent, ResourceContents, TextContent)):(line 2545), matches, and the next statement runsresource_id=getattr(content, "id")(line 2549) with no default, raisingAttributeError. There is no early return between the direct_proxy fetch and that call, so every direct_proxy text or blob read hits it. The handler instreamablehttp_transport.pycatchesExceptionand returns"".The test suite works around this by monkey-patching
id-bearing subclasses over the real models (tests/unit/mcpgateway/services/test_resource_service.py~7127), with a comment noting the post-direct-proxy code callsgetattr(content, "id")unconditionally — so the production classes that lackidtake a path the tests never exercise.A fix would return the constructed
TextResourceContents/BlobResourceContentsfrom the direct_proxy branch directly rather than routing it through the legacygetattr(content, "id")post-processing, or guard that block withgetattr(content, "id", None)plus a blob-aware path. A test asserting that a direct_proxy text read and animage/pngread both return content end-to-end would cover it.