Skip to content

Function parameter coercion ("false" => False) failures in properly documented FastMCP tools? #1843

Closed as not planned

Description

@davidbernat

Initial Checks

Description

Something does not square for me about the MCP Protocol standard, as owned an operated by Anthropic.

Here is the bug. I have a simple 20-line function that receives an image URL and applies a custom OCR toolkit. This is well-documented code with simple parameter structure. This runs locally (in this case). For those who do not want to read the code, there is no magic here: the result is a simple dictionary with a text field containing the OCR string extracted from the image. Optional flag add_token adds a further JSON dictionary of metadata about each token, add_typographical adds pixel data about the image font, and filter_as_block and confidence_threshold are simple quality filters. References to CloudNode (our proprietary internal infrastructure) do not contribute to this except to properly download the file (to a BytesIO object). Nothing surprising in any way.

Sitting here at this coffeeshop this morning, the MCP server has begun failing in two MCP Protocol ways.

    def ocr(
        image_url: str,
        add_tokens=True,
        add_typographical=False,
        filter_as_block=False,
        confidence_threshold: float = 0.50,
    ) -> dict:
        """
        Extract text from an image using Remarkable OCR.

        This tool performs optical character recognition on the provided
        image using the RemarkableOCR engine. It supports multiple output formats
        and filtering options to customize the OCR results for different use cases.

        Args:
            image_url (str): Image URL (http://, https://, file://, or uri://)
            add_tokens (bool, optional): Include raw OCR tokens in response.
                Defaults to True. When True, adds detailed token information.
            add_typographical (bool, optional): Include typographical statistics.
                Defaults to False. When True, adds enriched typographical data.
            filter_as_block (bool, optional): Filter text using block assumptions.
                Defaults to False. When True, applies block-based text filtering.
            confidence_threshold (float, optional): Minimum confidence threshold
                for OCR recognition. Defaults to 0.50.

        Returns:
            dict: CloudNode format response containing:
                - ok (bool): Operation success status
                - msg (str): Status message or error description
                - data (dict): OCR result with:
                    - text (str): Readable text lines extracted from the image
                    - tokens (list, optional): Raw OCR tokens if add_tokens=True
                    - typographical (dict, optional): Typographical statistics if add_typographical=True

        Note:
            The image input should be a URL using CloudNode's supported protocols.
            RemarkableOCR handles all image format conversion internally.
        """
  1. The LLM is constructing input calls to use parameters set by, for instance, add_tokens=false, instead of the proper Python boolean False. The downstream OCR MCP server tool (my code) is interpreting this as the string 'false', when checked with a reasonable if add_token:, and so the OCR software adds the tokens to the result (and succeeds, as the code is well-developed and properly maintained). However, this an incorrect invocation of the MCP tool by the LLM as instructed by the MCP Protocol communication. I must presume that not only does the MCP server directly specify that its software is operating Python (in this case), but the docstring of the function is clearly sufficient for a large LLM (Big Pickle by Zen) to not make this error. The only improvement to my OCR code would be type enforcing, i.e., add_tokens: bool=False in the ocr() function declaration.
cloudnode-home_ocr [image_url=file:///twain.jpg, filter_as_block=true, add_tokens=false, add_typographical=false]
  1. The results returned by the ocr() function are significantly large when add_tokens=True (roughly 500 characters for each token) and dumping all this information into the LLM context breaks the LLM session. This behavior between LLMs and MCPs continues to confuse me as to why this failure mode does not have a mature design pattern already. Forgive my agog, but this is day one engineering; and you are responsible for an industry design pattern (and we already see Amazon fingers a lot of stewing problems). Yes, technically, the responsibility for this error is the LLM itself choosing to dump the result to context (OpenCode executed), and the LLM 'should' be aware enough to write this data to a temporary file which is then selectively read in a subagent session owning its own context, ultimately summarizing its results to the primary agent to continue in its original context. But, kindly, why on Earth would MCP Protocol standards (and OpenCode) not be designed to do this by default, from the program management onset, to create provenance and control over API and external functions. Be serious. There is no reason the LLM could not communicate the maximum extent of its context, and the MCP Protocol be aware that the response needs to be cached on disk instead, or this be a core configuration of any MCP server on the client side for simple bookkeeping, reconstruction, and provenance. (This surely would have led OpenCode to design their agents systems to expect on disk file interactions and caching, as well, and you guys are all smarter than me.) As for the problem-as-is manifest on the OpenCode side (without such changes to MCP Protocol), a standard subagent could simply dump the entire file back into the context back to the agent. And, yes, OpenCode agent paradigms are, of course, not the responsibility of the MCP Protocol, nor its team. But MCP Protocol is responsible on the client side, and so not allowing configurations out of the box to the response and dumping to a temporary file instead of filtrating up to the agent is a design choice of the MCP Protocol. To really highlight the insolvent security issues inherent in the MCP + Agent paradigms of the industry today: in this case, in the failure mode of the LLM (Big Pickle) response is to, unbelievably, attempt to execute custom Python code to solve the OCR problem. This makes no sense within a robust execution environment for software implementation, and I am surely not the first person to raise this discussion here. Thanks. You guys are the best.

Google is strap-hung to return Reddit forums instead of proper results (or forbid GitHub issues), but here is the same problem from a different user six days ago. Not only do the standard solutions require total control of the MCP server tool code itself, these require intense, and standardized, data handling on the server side across the entire tool suite, PLUS the LLM to do intense heavy lifting with foreign files and pagination. You guys are making these design choices for Amazon at Anthropic and calling this open source, which clearly this has no reckoning to be. And the best Google can do is cough up an OpenAI-cohort chat between future bots. Can we agree on that as an industry? Thanks. You guys are the best. https://www.reddit.com/r/mcp/comments/1q2lm5f/comment/nxgfv5b/

Image Image

Thanks. You guys are the best.
Property of Starlight LLC (as is every piece of content created by my software)

Python & MCP Python SDK

Python 3.13.9
MCP 1.25
OpenCode 1.1.8

update 11:48AM
Indeed, having improved the my own code to enforce types, there is no transparency into the MCP coercion logic. As you can see here, the LLM display output is similar to that before (using the lowercase boolean and no quotes) but the parameters passed to the MCP tool are now booleans. So there is no native inspection into what the MCP Protocol is doing for many of the most important LLM use cases, especially for naive users. Indeed, during debugging the MCP server executed a timeout, and the LLM began writing custom Python code to execute its own filesystem reads and third-party OCR packages (pytesseract, in this case) which is an insane failure mode. You cannot go to an enterprise manager and tell them this, so I question what this industry is really doing. Every vibe-coding joke on the web starts with intern analysts putting OpenAI API calls into try/catch statements on JS pages. Be serious. Thanks. You guys are the best.

Image Image

Thanks. You guys are the best.

Activity

  1. added a commit that references this issue on Jan 12, 2026
    674d7b4
  2. maxisbey commented on Jan 13, 2026

    @maxisbey
    Contributor

    Related Typescript issue: modelcontextprotocol/typescript-sdk#1361

    Not entirely sure how or whether we'd want to handle this. Had some internal discussions around it with some open questions:

    • Should this be server side or client side coercion?
    • If it's client side, how do we get the tool definition reliably? Do we just cache it if a list tools happens first?
    • Does this require a SEP first?

    Another note is this really only seems to affect the FastMCP module, you can do type coercion yourself with the lowlevel api.

  3. davidbernat commented on Jan 13, 2026

    @davidbernat
    Author

    Great questions, @maxisbey , but I have epic equestions about your framing so please indulge.

    If I understand correctly, the MCP Server publishes its tool definitions at first connection. My understanding is that the entire purpose of MCP Protocol is to manage getting the tool definition reliably. Why is your question an issue (or not covered by MCP definition)?

    Regarding client-side or server-side: I cannot make a judgement call on whether the logic of "is tool Python? => apply Python type coercion => do the resulting arguments match the interface signature of the requested tool? ==> fail if not, otherwise send".

    It would seem that since all the logic is available client-side (and MCP Protocol is responsible for keeping fresh tool definitions) that client-side is more secure, faster, and bandwidth appropriate. But something feels off that this is even a question here.

    In the meantime, could you point to the specific location that the client-side lowlevel api would do this type munging? In these last several months, the feeling of using these modern tools is that a black box shrouds the packages' logics for prompt & response coercion within these various systems (MCP, PydanticAI, OpenCode). Naturally, we now live in a new world where prompt engineering (coercion) is a hard-fought tacit and proprietary domain IP, but these are all open source packages. If you asked me to pinpoint how PydanticAI modifies a prompt for a simple type-checking, code inspection immediately drops out into patterns of abstraction. If you asked me how OpenCode uses tool responses, or even what input fields are in event-specific tool calls, I would groan. I have been software engineering for more than a decade and this new practice feels circumspect.

    Happy to do the PR if I am up to the task and you are asking.

  4. maxisbey commented on Feb 10, 2026

    @maxisbey
    Contributor

    We won't be adding this into the low level Server class as type coercion can actually be done yourself. As for MCPServer (we've renamed it from FastMCP in the new main branch), I don't know whether we want to add this. Would need more evidence this is a widely needed feature and maybe align with the other SDKs on what to do here.

  5. added
    enhancementRequest for a new feature that's not currently supported
    needs confirmationNeeds confirmation that the PR is actually required or needed.
    on Feb 10, 2026
  6. davidbernat commented on Feb 13, 2026

    @davidbernat
    Author

    @maxisbey Noted. Do you have a working group email exchange? MCP Protocol by its nature feels very flawed to me.

    Why is there not a method for transmitting files data through base64 encoding underneath the LLM context prompt?
    REST APIs built the Internet by supplying that path interface. Most MCPs are candy-coated wrappers to APIs.
    It is a breaking chance the scale of the Internet.

    As for type coercion at the client level, it speaks to a flaw in the MCP Protocol design of what is communicated to the LLM to provide strict limitations on its exporting of parameters. Every LLM that gets LSP argument generation wrong is doing to want to know at the LLM client interface; both for training purposes and shortest path to extremely easy to identify failure modes. I cannot see why a round trip to a third-party server system, parameter validation and error reporting, and transmission back to the LLM as a generic JSON of errors, all part of the responsibility of every third-party developer on every MCP/API interface is in its design of the MCP Protocol. If the MCPs are using LSPs this comes free, and if not, I just do not trust it anyway. Thanks.

    Here is an absolutely prime example of a failure I cannot see MCP Protocol persisting through:

    Today I processed a lawsuit research dossier that obtains the original legal filing on the web in PDF form via URL. An existing OCR/PDF API sits on my server in my house office. And I cannot wrap that API in an MCP to handle local files without uploading to a central self-hosted cloud system. That technical debt means that that API is not tested and running as an MCP. That URL document tonight needs to be downloaded to my computer, which an LLM native-text cannot handle, and then its URL passed to that OCR/PDF MCP, which does not yet exist for the reasons above. Both of those functions are out-of-the-box REST API, industry standard base64 encoding of APIs which built the Internet, and which come "free with MCP Protocol" if that one standard is adopted, knowing that the data MCP Protocol transmits is already base64 encoded because its APIs operate on the same standards as the Internet. I am unable to see how everything in this discussion is not an artificial boundary created by the MCP Protocol logic and community. I think we understand why, but to the users I think we know their use cases are not served.

  7. maxisbey commented on Feb 13, 2026

    @maxisbey
    Contributor

    For the lowlevel Server class; type coercion doesn't make sense to support here as the Server class has no way of reliably knowing the definition of a tool during an individual tool call for many reasons. But, server implementers are 100% able to implement this themselves without having to do any weird workarounds.

    Example code of low level Server type coercion
    from pydantic import BaseModel
    
    from mcp import types
    from mcp.server.context import ServerRequestContext
    from mcp.server.lowlevel.server import Server
    
    
    class AddArgs(BaseModel):
        a: float
        b: float
    
    
    TOOLS = {
        "add": (
            types.Tool(name="add", description="Add two numbers", input_schema=AddArgs.model_json_schema()),
            AddArgs,
        ),
    }
    
    
    async def handle_list_tools(
        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
    ) -> types.ListToolsResult:
        return types.ListToolsResult(tools=[tool for tool, _ in TOOLS.values()])
    
    
    async def handle_call_tool(
        ctx: ServerRequestContext, params: types.CallToolRequestParams
    ) -> types.CallToolResult:
        tool, model_class = TOOLS[params.name]
        args = model_class.model_validate(params.arguments or {})  # type coercion happens here
        return types.CallToolResult(content=[types.TextContent(type="text", text=str(args.a + args.b))])
    
    
    server = Server("example", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)

    There is no email group exchange, there is a Discord server though in which a lot of discussions including about the individual SDKs happen.

    MCPServer doesn't currently support this, but would be a more reasonable place to put this if we did have this available server side.

    As for client side type coercion, you could do this yourself as well, each time you call a tool first look up the tool definition (either re-request it, or cache locally), then do type coercion before calling.

    As for file uploading, we discussed this before, it's also unrelated to this issue.

  8. maxisbey commented on Aug 10, 2026

    @maxisbey
    Contributor

    For a bool-annotated tool parameter the generated argument model coerces the string "false" to False via pydantic; add_tokens=True here had no annotation, so the inputSchema advertises it as a string and the value passes through unchanged — annotating it (add_tokens: bool = True) is the fix, as the follow-up above confirms. Closing as part of a general backlog cleanup following the v2 release. If it's still relevant, feel free to reopen.

    AI Disclaimer

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

    enhancementRequest for a new feature that's not currently supportedneeds confirmationNeeds confirmation that the PR is actually required or needed.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions