Issue: FastMCP resources cannot return meta in read responses, but OpenAI client expects it

m using FastMCP and defining a resource like this:

@mcp.resource(
    uri=WIDGETS_BY_ID["search-insurance"].template_uri,
    name="search-insurance",
    description=_resource_description(WIDGETS_BY_ID["search-insurance"]),
    mime_type=MIME_TYPE,
    meta=_widget_resource_meta()
)
def search_insurance_widget() -> str:
    return WIDGETS_BY_ID["search-insurance"].html

According to the MCP spec and FastMCP documentation, the meta parameter is attached to the resource definition, and is supposed to appear when listing resources (resources/list or templates/list). It is not included in the resources/read response, which only returns the file contents.

However, the problem is:

When the OpenAI client calls mcp.read, it expects the meta field to be present in the response.
Because FastMCP does not return meta in read, the OpenAI client reports a missing field or does not behave as expected.

This makes it impossible to correctly implement certain MCP resources (for example, widget resources) using FastMCP, because the OpenAI client assumes a meta field that FastMCP will never include in the read response.

It seems like:

  • FastMCP is following the MCP spec correctly

  • But the OpenAI client is expecting behavior that doesn’t match the spec (or at least isn’t supported by FastMCP)

So right now, there is no way to satisfy the OpenAI client’s expectations using FastMCP, unless I manually inject meta information into the resource contents, which breaks the intended protocol.

Could the OpenAI team clarify whether:

  1. The meta field should be included in resources/read responses (meaning FastMCP needs an update),
    or

  2. The OpenAI client should not expect meta from read, and the current behavior is a bug?

This mismatch currently prevents proper widget integration when using FastMCP (CSP Issue)

Thanks!

3 Likes

It’s very messy, but in their Python FastMCP examples on Github it looks like they override the normal FastMCP behaviour with custom handlers and include _meta in the response.

e.g,

async def _handle_read_resource(req: types.ReadResourceRequest) -> types.ServerResult:
    widget = WIDGETS_BY_URI.get(str(req.params.uri))
    if widget is None:
        return types.ServerResult(
            types.ReadResourceResult(
                contents=[],
                _meta={"error": f"Unknown resource: {req.params.uri}"},
            )
        )

    contents = [
        types.TextResourceContents(
            uri=widget.template_uri,
            mimeType=MIME_TYPE,
            text=widget.html,
            _meta=_tool_meta(widget),
        )
    ]

    return types.ServerResult(types.ReadResourceResult(contents=contents))

It works, but dodgy… hopefully either OpenAI aligns to the spec or the spec (and FastMCP) get updated for consistency.

1 Like

This is similar to what I’m planning to do, but going to keep the original handler call to hopefully future proof it a little bit (really don’y like overriding these private methods when FastMCP is moving so fast!).

Override this method on a subclassed FastMCP:

def _setup_read_resource_handler(self) -> None:
    """
    Override to include resource meta in read_resource responses.
    og
    https://github.com/jlowin/fastmcp/blob/2f561ec98d0b5f078221acf6a2126b1152966365/src/fastmcp/server/server.py#L679
    """
    super()._setup_read_resource_handler()
    original_handler = self._mcp_server.request_handlers[ReadResourceRequest]

    async def wrapper(req: ReadResourceRequest) -> ServerResult:
        uri = req.params.uri

        # Look up resource first to get meta
        resource = None
        resource_meta = None
        async with Context(fastmcp=self):
            resource = await self._get_resource_or_template_or_none(str(uri))
            if resource and self._should_enable_component(resource):
                resource_meta = resource.get_meta(
                    include_fastmcp_meta=self.include_fastmcp_meta
                )

        # Use parent handler for task routing and reading
        result = await original_handler(req)

        # Rebuild contents with meta (Pydantic models are immutable)
        read_result = result.root
        if (
            resource_meta
            and isinstance(read_result, ReadResourceResult)
            and read_result.contents
        ):
            new_contents = []
            for content in read_result.contents:
                existing_meta = getattr(content, "_meta", None) or {}
                merged_meta = {**resource_meta, **existing_meta}
                if hasattr(content, "text"):
                    new_contents.append(
                        TextResourceContents(
                            uri=content.uri,
                            text=content.text,
                            mimeType=content.mimeType,
                            _meta=merged_meta,
                        )
                    )
                elif hasattr(content, "blob"):
                    new_contents.append(
                        BlobResourceContents(
                            uri=content.uri,
                            blob=content.blob,
                            mimeType=content.mimeType,
                            _meta=merged_meta,
                        )
                    )
                else:
                    new_contents.append(content)
            return ServerResult(ReadResourceResult(contents=new_contents))

        return result

    self._mcp_server.request_handlers[ReadResourceRequest] = wrapper