Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions langchain_mcp_adapters/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This module provides functionality to convert MCP tools into LangChain-compatible
tools, handle tool execution, and manage tool conversion between the two formats.
"""

from copy import deepcopy
from typing import Any, cast, get_args

from langchain_core.tools import (
Expand Down Expand Up @@ -150,6 +150,17 @@ def convert_mcp_tool_to_langchain_tool(
msg = "Either a session or a connection config must be provided"
raise ValueError(msg)

input_schema = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FerroLx thanks for the PR: The place to fix this is likely langchain_core. Could you check if the issue exists in langchain_core / if you can reproduce it there?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are correct!

the error is from the core.

it reproduces with this script

import os
from dotenv import load_dotenv
from langchain_core.tools import StructuredTool
from langchain_google_genai import ChatGoogleGenerativeAI

load_dotenv()

# This buggy schema simulates a tool definition from an external source
# where the 'type' for an enum is missing.
buggy_schema = {
    "title": "MyTool",
    "description": "A tool with a buggy enum schema.",
    "type": "object",
    "properties": {
        "param": {
            "description": "A parameter with an enum but no type.",
            "enum": ["a", "b"],
        }
    },
    "required": ["param"],
}


def my_tool_function(param: str):
    """A simple tool to demonstrate the issue."""
    return f"You selected {param}"


try:
    # We define the tool directly with the buggy dictionary schema
    tool = StructuredTool(
        name="MyTool",
        description="A tool with a buggy enum schema.",
        func=my_tool_function,
        args_schema=buggy_schema,
    )

    # Initialize the Vertex AI Chat Model
    # Make sure you are authenticated with Google Cloud CLI
    # and have set your project in the environment variables.
    # e.g., os.environ["GCLOUD_PROJECT"] = "your-gcloud-project"
    model = ChatGoogleGenerativeAI(model="gemini-2.5-flash")

    # Bind the tool to the model
    model_with_tools = model.bind_tools([tool])

    # This will raise an error when the tool schema is processed by the Google API
    print("Invoking model with tool...")
    model_with_tools.invoke("Select option a")

except Exception as e:
    print(f"Successfully reproduced the bug with langchain-google: {e}")

I've found the solution!

It's just do this!

def _convert_json_schema_to_openai_function(
    schema: dict,
    *,
    name: Optional[str] = None,
    description: Optional[str] = None,
    rm_titles: bool = True,
) -> FunctionDescription:
    """Converts a Pydantic model to a function description for the OpenAI API.

    Args:
        schema: The JSON schema to convert.
        name: The name of the function. If not provided, the title of the schema will be
            used.
        description: The description of the function. If not provided, the description
            of the schema will be used.
        rm_titles: Whether to remove titles from the schema. Defaults to True.

    Returns:
        The function description.
    """
    schema = dereference_refs(schema)
    if "definitions" in schema:  # pydantic 1
        schema.pop("definitions", None)
    if "$defs" in schema:  # pydantic 2
        schema.pop("$defs", None)

    # FIX: Ensure that any property with an 'enum' also has a 'type'
    if "properties" in schema:
        for prop in schema["properties"].values():
            if "enum" in prop and "type" not in prop:
                prop["type"] = "string"

    title = schema.pop("title", "")
    default_description = schema.pop("description", "")
    return {
        "name": name or title,
        "description": description or default_description,
        "parameters": _rm_titles(schema) if rm_titles else schema,
    }

Do you want me to do a pr for it or you'll do it?

@eyurtsev

if isinstance(tool.inputSchema, dict):
input_schema = deepcopy(tool.inputSchema)
if "properties" in input_schema:
for _, prop_schema in input_schema.get("properties", {}).items():
if "enum" in prop_schema and "type" not in prop_schema:
prop_schema["type"] = "string"
else:
input_schema = tool.inputSchema


async def call_tool(
**arguments: dict[str, Any],
) -> tuple[str | list[str], list[NonTextContent] | None]:
Expand Down Expand Up @@ -255,7 +266,7 @@ async def call_tool(
return StructuredTool(
name=tool.name,
description=tool.description or "",
args_schema=tool.inputSchema,
args_schema=input_schema,
coroutine=call_tool,
response_format="content_and_artifact",
metadata=metadata,
Expand Down