Skip to content

direct_proxy resource read raises AttributeError ('…ResourceContents' has no attribute 'id') and returns empty content #5451

Description

@jedwards1230

🐞 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

  • Federation or Transports

🔁 Steps to Reproduce

  1. Set MCPGATEWAY_DIRECT_PROXY_ENABLED=true and restart the gateway.
  2. 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).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    COULDP3: Nice-to-have features with minimal impact if left out; included if time permits

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions