Skip to content

It doesn't work with MCP tools #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
n-sviridenko opened this issue May 15, 2025 · 9 comments
Open

It doesn't work with MCP tools #17

n-sviridenko opened this issue May 15, 2025 · 9 comments
Assignees

Comments

@n-sviridenko
Copy link

The MCP adapters code

https://github.com/langchain-ai/langchain-mcp-adapters/blob/94f0588ca86a40737d16a8e8d6adf84520dcd807/langchain_mcp_adapters/tools.py#L41C5-L41C39

outputs to the sandbox:


    async def call_tool(
        **arguments: dict[str, Any],
    ) -> tuple[str | list[str], list[NonTextContent] | None]:
        call_tool_result = await session.call_tool(tool.name, arguments)
        return _convert_call_tool_result(call_tool_result)

and due to the indentation, code execution fails.

@n-sviridenko
Copy link
Author

Is the sandbox linked to the external environment in any way, or is it completely isolated (meaning the entire tool's code needs to be copied)?

@n-sviridenko
Copy link
Author

n-sviridenko commented May 16, 2025

I've put the whole MCP logic inside, but now result is always None for some reason:

PyodideSandbox response structure: ['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__match_args__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'execution_time', 'result', 'status', 'stderr', 'stdout']
PyodideSandbox stdout: None
PyodideSandbox stderr: None
PyodideSandbox result type: <class 'NoneType'>

source code:

from typing import Dict, List, Any, Callable, Optional
import inspect
import re
import json
from langchain_core.messages import AIMessage
from langgraph_codeact import EvalCoroutine
from langchain_sandbox import PyodideSandbox
from langchain_core.tools import BaseTool

sandbox = PyodideSandbox("./sessions", allow_net=True, allow_run=True, allow_ffi=True)

def make_safe_function_name(name: str) -> str:
    """Convert a tool name to a valid Python function name."""
    # Replace non-alphanumeric characters with underscores
    safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
    # Ensure the name doesn't start with a digit
    if safe_name and safe_name[0].isdigit():
        safe_name = f"tool_{safe_name}"
    # Handle empty name edge case
    if not safe_name:
        safe_name = "unnamed_tool"
    return safe_name

def create_pyodide_eval_fn(
    session_id: str | None = None,
    mcp_tools: List[BaseTool] = None, # from client.get_tools()
    mcp_config: Dict[str, Any] = None
) -> EvalCoroutine:
    """Create an eval_fn that uses PyodideSandbox.

    Args:
        session_id: ID of the session to use
        mcp_tools: List of MCP tools to make available in the sandbox
        mcp_config: MCP server configuration 

    Returns:
        A function that evaluates code using PyodideSandbox
    """
    mcp_tools = mcp_tools or []
    mcp_config = mcp_config or {}
    
    # Get the list of MCP tool names
    mcp_tool_names = [tool.name for tool in mcp_tools if hasattr(tool, "name")]
    
    # Get make_safe_function_name source code for inclusion in the sandbox
    make_safe_function_name_src = inspect.getsource(make_safe_function_name)
    
    async def async_eval_fn(code: str, _locals: dict[str, Any]) -> tuple[str, dict[str, Any]]:
        # Create a wrapper function that will execute the code and return locals
        # Initialize MCP server and tools directly in the wrapper code
        wrapper_code = f'''
# MCP tool initialization
from langchain_mcp_adapters.client import MultiServerMCPClient
import json
import os
import re

{make_safe_function_name_src}

async def execute():
    try:
        # MCP configurations from parent environment
        __mcp_config = {json.dumps(mcp_config)}
        
        # Define MCP tools as variables in the sandbox
        async with MultiServerMCPClient(__mcp_config) as __mcp_client:
            __mcp_tools = __mcp_client.get_tools()
            # Create variables for each MCP tool
            for __tool in __mcp_tools:
                if hasattr(__tool, "name"):
                    __safe_name = make_safe_function_name(__tool.name)
                    globals()[__safe_name] = __tool
            
            # Execute the provided code
{chr(10).join("            " + line for line in code.strip().split(chr(10)))}
            result = locals()
            print("execute() function result keys: ", list(result.keys()))
            return result
    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print("Error in execute(): ", str(e))
        print("Traceback: ", error_details)
        return {{"error": str(e) + " Traceback: " + str(error_details)}}

return await execute()
'''
        # Convert functions in _locals to their string representation
        context_setup = ""
        safe_mcp_tool_names = [make_safe_function_name(tool_name) for tool_name in mcp_tool_names]
        for key, value in _locals.items():
            if callable(value):
                if key not in safe_mcp_tool_names:
                    # Get the function's source code for non-MCP tools
                    src = inspect.getsource(value)
                    context_setup += f"\n{src}"
            else:
                context_setup += f"\n{key} = {repr(value)}"

        try:
            # Execute the code and get the result
            response = await sandbox.execute(
                code=context_setup + "\n\n" + wrapper_code,
                session_id=session_id,
            )

            # Check if execution was successful
            if response.stderr:
                return f"Error during execution: {response.stderr}", {}

            # Get the output from stdout
            output = (
                response.stdout if response.stdout else "<Code ran, no output printed to stdout>"
            )
            
            # Added debugging logs
            print(f"PyodideSandbox response structure: {dir(response)}")
            print(f"PyodideSandbox stdout: {response.stdout}")
            if hasattr(response, 'stderr'):
                print(f"PyodideSandbox stderr: {response.stderr}")
            
            result = response.result
            print(f"PyodideSandbox result type: {type(result)}")

            # If there was an error in the result, return it
            if isinstance(result, dict) and "error" in result:
                return f"Error during execution: {result['error']}", {}

            # Check if result is None before trying to access items()
            if result is None:
                return f"Error during execution: Python code execution returned None result", {}

            # Get the new variables by comparing with original locals
            new_vars = {
                k: v for k, v in result.items() if k not in _locals and not k.startswith("_")
            }
            return output, new_vars

        except Exception as e:
            return f"Error during PyodideSandbox execution: {repr(e)}", {}

    return async_eval_fn

@n-sviridenko
Copy link
Author

n-sviridenko commented May 16, 2025

Removing this line makes it run: from langchain_mcp_adapters.client import MultiServerMCPClient (and fail due to other reasons at least)

So the issue is somewhere on micropip level.

@n-sviridenko
Copy link
Author

I tested it out in the console in isolation and there are no issues with that library:

https://pyodide.org/en/latest/console.html

Welcome to the Pyodide 0.28.0a2 terminal emulator 🐍
Python 3.13.2 (main, May 15 2025 15:53:46) on WebAssembly/Emscripten
Type "help", "copyright", "credits" or "license" for more information.
>>> import micropip
await micropip.install('langchain_mcp_adapters')
from langchain_mcp_adapters.client import MultiServerMCPClient
>>> MultiServerMCPClient
<class 'langchain_mcp_adapters.client.MultiServerMCPClient'>
>>> 

@n-sviridenko
Copy link
Author

But in the version inside langchain-sandbox (0.27.4), it fails:

Welcome to the Pyodide 0.27.4 terminal emulator 🐍
Python 3.12.7 (main, Mar 18 2025 13:27:55) on WebAssembly/Emscripten
Type "help", "copyright", "credits" or "license" for more information.
>>> import micropip
await micropip.install('langchain_mcp_adapters')
from langchain_mcp_adapters.client import MultiServerMCPClient
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/lib/python3.12/site-packages/micropip/package_manager.py", line 133, in install
    return await install(
           ^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/install.py", line 53, in install
    await transaction.gather_requirements(requirements)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 55, in gather_requirements
    await asyncio.gather(*requirement_promises)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 62, in add_requirement
    return await self.add_requirement_inner(Requirement(req))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 152, in add_requirement_inner
    await self._add_requirement_from_package_index(req)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 214, in _add_requirement_from_package_ind
ex
    await self.add_wheel(wheel, req.extras, specifier=str(req.specifier))
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 262, in add_wheel
    await asyncio.gather(
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 55, in gather_requirements
    await asyncio.gather(*requirement_promises)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 59, in add_requirement
    return await self.add_requirement_inner(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 152, in add_requirement_inner
    await self._add_requirement_from_package_index(req)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 214, in _add_requirement_from_package_ind
ex
    await self.add_wheel(wheel, req.extras, specifier=str(req.specifier))
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 262, in add_wheel
    await asyncio.gather(
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 55, in gather_requirements
    await asyncio.gather(*requirement_promises)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 59, in add_requirement
    return await self.add_requirement_inner(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 152, in add_requirement_inner
    await self._add_requirement_from_package_index(req)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 214, in _add_requirement_from_package_ind
ex
    await self.add_wheel(wheel, req.extras, specifier=str(req.specifier))
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 262, in add_wheel
    await asyncio.gather(
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 55, in gather_requirements
    await asyncio.gather(*requirement_promises)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 59, in add_requirement
    return await self.add_requirement_inner(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 152, in add_requirement_inner
    await self._add_requirement_from_package_index(req)
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 204, in _add_requirement_from_package_ind
ex
    wheel = find_wheel(metadata, req)
            ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.12/site-packages/micropip/transaction.py", line 316, in find_wheel
    raise ValueError(
ValueError: Can't find a pure Python 3 wheel for 'zstandard<0.24.0,>=0.23.0'.
See: https://pyodide.org/en/stable/usage/faq.html#why-can-t-micropip-find-a-pure-python-wheel-for-a-package
You can use `await micropip.install(..., keep_going=True)` to get a list of all packages with missing wheels.
Traceback (most recent call last):
  File "<console>", line 1, in <module>
ModuleNotFoundError: No module named 'langchain_mcp_adapters'
>>> 

@n-sviridenko
Copy link
Author

Most likely the latest version already contains it: pyodide/pyodide#4730 (comment)

@n-sviridenko
Copy link
Author

n-sviridenko commented May 16, 2025

The current problem is that the 0.28 doesn't output stdout same way as 0.27.X (based on examples/codeact_agent.py).

0.28:

The ball lands 156.57 meters away from the batter.
After the outfielder's throw, the ball lands 98.51 meters away from where the batter hit it.

0.27.X:

{"stdout":"The baseball lands 156.73 m away from the batter.After the outfielder throws the ball, it lands 98.61 m from the batter's original position.The baseball is 98.61 m from where the batter originally hit it.","stderr":null,"result":{"initial_velocity":45.847,"launch_angle_degrees":23.474,"g":9.8,"launch_angle_radians":0.4096985886131489,"v0x":42.05274475933024,"v0y":18.262367513732254,"time_in_air_1":3.727013778312705,"horizontal_distance_1":156.7311591338912,"throw_velocity":24.12,"throw_angle_degrees":39.12,"throw_angle_radians":0.6827728033801816,"vx_throw":18.71292824319701,"vy_throw":15.218433446480612,"time_in_air_2":3.1058027441797162,"horizontal_distance_2":58.11866388935939,"final_position":98.6124952445318,"distance_from_origin":98.6124952445318},"success":true}

@eyurtsev
Copy link
Collaborator

@n-sviridenko thanks for the report we'll take a look.

@eyurtsev eyurtsev transferred this issue from langchain-ai/langgraph-codeact May 16, 2025
@eyurtsev
Copy link
Collaborator

@n-sviridenko the newest release doesn't install langchain still i haven't updated to the pre-release of pyodide, but it won't swallow the error anymore.

I'll review the prerelease next week to determine if I'm OK changing to it

@eyurtsev eyurtsev self-assigned this May 16, 2025
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

No branches or pull requests

2 participants