Skip to content

Fix MCP resources return type#31

Open
adarshdigievo wants to merge 5 commits intomainfrom
fix/mcp-resources-return-type
Open

Fix MCP resources return type#31
adarshdigievo wants to merge 5 commits intomainfrom
fix/mcp-resources-return-type

Conversation

@adarshdigievo
Copy link
Copy Markdown
Member

Issue

When testing SerpApi MCP (currently deployed server at mcp.serpapi.com), I found out that the read operation on MCP resources for all engines was failing.

image

When tested with the official MCP inspector tool, this was confirmed.

image

Also, this CURL request can be used to reproduce the issue.

curl 'http://mcp.serpapi.com/123/mcp' \
  -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'Cache-Control: no-cache' \
  -H 'Connection: keep-alive' \
  -H 'Pragma: no-cache' \
  -H 'accept: application/json, text/event-stream' \
  -H 'content-type: application/json' \
  --data-raw '{"method":"resources/read","params":{"uri":"serpapi://engines/google_shopping_filters","_meta":{"progressToken":3}},"jsonrpc":"2.0","id":3}' \
  --insecure

Any dummy value can be used in place of the API Key for testing (In the above example, I used 123 in place of the key) since we are running MCP-level operations and no actual SerpApi requests are performed. The same failure occurs even with a working API key.

The bug

This was an interesting bug. I tried replicating the issue locally, but everything was working well locally. Looking at the latest FastMCP docs (https://gofastmcp.com/servers/resources#the-@resource-decorator), I saw that MCP resources are now recommended to be returned as raw strings or as the newly introduced ResourceResult type.

Currently, our repo returns resource results as Python dictionaries.

def _engine_resource_factory(engine: str, engine_path: Path) -> Resource:
    def _load_engine() -> dict[str, Any]:
        return json.loads(engine_path.read_text())

    return Resource.from_function(
        fn=_load_engine,
        uri=f"serpapi://engines/{engine}",
        name=f"serpapi-engine-{engine}",
        description=f"SerpApi engine specification for {engine}.",
        mime_type="application/json",
    )

I checked whether this was causing an error in 2.* versions of FastMCP, as well as the latest version (3.2.4) - the bug was not present, and everything worked perfectly.

The real issue was that our latest deployment was on 12th March, and the FastMCP version, which was the latest at that time (v3.2.0), had the return types strictly enforced. The version expects a string output, but we were returning a dict (return json.loads(engine_path.read_text())), which was previously supported.

We were using uv sync command in our Dockerfile without copying UV lockfile, and this caused the latest available version of packages to be installed at deployment time.

The issue can be reproduced locally by:

uv pip install fastmcp==3.1.0 # to forcefully install buggy version

# then running
uv run --no-sync src/server.py # to start the server without updating the dependency version

Fixes

This PR does the following as fixes/enhancements to our current flow. A simple update/redeploy will be enough to fix the issue for now, but the steps below were taken for future-proofing.

  • Added UV lockfile to Docker and use uv sync --locked to install exact package versions from the lockfile. This will help with deterministic installs and help prevent new supply chain vulnerabilities on redeployments.

  • Upgraded FastMCP to the latest version: The version added new explicit types to express MCP Resource outputs.

  • ResourceResult objects are used to wrap responses of MCP resource calls. This makes the implementation more explicit. Ref: https://gofastmcp.com/servers/resources#resourceresult

  • Explicit tool description: The latest version of FastMCP was stripping the tool description supplied as a Python docstring. I have extracted the tool description into a new variable and made a few edits to improve clarity and align the terminology with the MCP specification. Then passed this explicitly to the tool decorator (as @mcp.tool(description=search_tool_description)).

Looking forward to your thoughts on these changes.

Also bumped the project's version to 0.3.0.

Testing the implementation

  • I tested the MCP server by running it using Docker. The MCP inspector is happy - All resources and the search tool return expected data
image
  • I also did a quick test with Codex, connecting it to the local MCP server. Codex could fetch the engine schema successfully.
image

Future Enhancements

  • Add tests in CI using the MCP Inspector CLI to verify that all resources and tools are returning expected results. I will start working on this once the current changes are finalized and merged.
  • Integrate with error tracking tools to get alerts on runtime issues.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant