├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── README.md ├── api-reference │ └── introduction.mdx ├── building-custom-agents.mdx ├── development.mdx ├── docs.json ├── essentials │ ├── configuration.mdx │ ├── connection-types.mdx │ ├── debugging.mdx │ ├── llm-integration.mdx │ └── server-manager.mdx ├── favicon.svg ├── images │ ├── hero-dark.png │ └── hero-light.png ├── introduction.mdx ├── logo │ ├── dark.svg │ └── light.svg ├── quickstart.mdx └── snippets │ └── snippet-intro.mdx ├── examples ├── airbnb_mcp.json ├── airbnb_use.py ├── blender_use.py ├── browser_mcp.json ├── browser_use.py ├── chat_example.py ├── filesystem_use.py ├── http_example.py └── multi_server_example.py ├── mcp_use ├── __init__.py ├── adapters │ ├── __init__.py │ ├── base.py │ └── langchain_adapter.py ├── agents │ ├── __init__.py │ ├── base.py │ ├── mcpagent.py │ └── prompts │ │ ├── system_prompt_builder.py │ │ └── templates.py ├── client.py ├── config.py ├── connectors │ ├── __init__.py │ ├── base.py │ ├── http.py │ ├── stdio.py │ └── websocket.py ├── logging.py ├── managers │ ├── __init__.py │ ├── server_manager.py │ └── tools │ │ ├── __init__.py │ │ ├── base_tool.py │ │ ├── connect_server.py │ │ ├── disconnect_server.py │ │ ├── get_active_server.py │ │ ├── list_servers_tool.py │ │ ├── search_tools.py │ │ └── use_tool.py ├── session.py └── task_managers │ ├── __init__.py │ ├── base.py │ ├── sse.py │ ├── stdio.py │ └── websocket.py ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── static └── image.jpg └── tests ├── conftest.py └── unit ├── test_client.py ├── test_config.py ├── test_http_connector.py ├── test_logging.py ├── test_session.py └── test_stdio_connector.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Description 2 | 3 | ## Changes 4 | 5 | Describe the changes introduced by this PR in a concise manner. 6 | 7 | ## Implementation Details 8 | 9 | 1. List the specific implementation details 10 | 2. Include code organization, architectural decisions 11 | 3. Note any dependencies that were added or modified 12 | 13 | ## Example Usage (Before) 14 | 15 | ```python 16 | # Include example code showing how things worked before (if applicable) 17 | ``` 18 | 19 | ## Example Usage (After) 20 | 21 | ```python 22 | # Include example code showing how things work after your changes 23 | ``` 24 | 25 | ## Documentation Updates 26 | 27 | * List any documentation files that were updated 28 | * Explain what was changed in each file 29 | 30 | ## Testing 31 | 32 | Describe how you tested these changes: 33 | - Unit tests added/modified 34 | - Manual testing performed 35 | - Edge cases considered 36 | 37 | ## Backwards Compatibility 38 | 39 | Explain whether these changes are backwards compatible. If not, describe what users will need to do to adapt to these changes. 40 | 41 | ## Related Issues 42 | 43 | Closes #[issue_number] 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Check Version Bump and Publish to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'pyproject.toml' 9 | 10 | # Required for PyPI trusted publishing 11 | permissions: 12 | id-token: write 13 | contents: write # Required for creating tags and releases 14 | 15 | jobs: 16 | check-version-and-publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 # This fetches all history for comparing versions 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: "3.11" 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install build twine wheel tomli 32 | 33 | - name: Check for version bump 34 | id: check-version 35 | run: | 36 | # Extract current version directly from pyproject.toml 37 | # This is more reliable than using importlib.metadata 38 | CURRENT_VERSION=$(python -c " 39 | import tomli 40 | with open('pyproject.toml', 'rb') as f: 41 | data = tomli.load(f) 42 | print(data['project']['version']) 43 | ") 44 | 45 | echo "Current version: $CURRENT_VERSION" 46 | 47 | # Check if this version already has a tag 48 | if git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then 49 | echo "Version $CURRENT_VERSION already has a tag. Skipping release." 50 | echo "is_new_version=false" >> $GITHUB_OUTPUT 51 | else 52 | echo "New version detected: $CURRENT_VERSION" 53 | echo "is_new_version=true" >> $GITHUB_OUTPUT 54 | echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 55 | fi 56 | 57 | - name: Build package 58 | if: steps.check-version.outputs.is_new_version == 'true' 59 | run: | 60 | python -m build 61 | 62 | - name: Create Release 63 | if: steps.check-version.outputs.is_new_version == 'true' 64 | id: create_release 65 | uses: actions/create-release@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | tag_name: v${{ steps.check-version.outputs.new_version }} 70 | release_name: Release v${{ steps.check-version.outputs.new_version }} 71 | draft: false 72 | prerelease: false 73 | 74 | - name: Publish to PyPI 75 | if: steps.check-version.outputs.is_new_version == 'true' 76 | uses: pypa/gh-action-pypi-publish@release/v1 77 | with: 78 | password: ${{ secrets.PYPI_API_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install .[dev,anthropic,openai,search] 26 | - name: Lint with ruff 27 | run: | 28 | ruff check . 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | db.sqlite3-journal 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # IPython 76 | profile_default/ 77 | ipython_config.py 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # pipenv 83 | Pipfile.lock 84 | 85 | # poetry 86 | poetry.lock 87 | 88 | # Environment variables 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | # VS Code 116 | .vscode/ 117 | *.code-workspace 118 | 119 | # PyCharm 120 | .idea/ 121 | *.iml 122 | 123 | # macOS 124 | .DS_Store 125 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.3.2 5 | hooks: 6 | - id: ruff 7 | args: [--fix, --exit-non-zero-on-fix, --config=ruff.toml] 8 | types: [python] 9 | - id: ruff-format 10 | args: [--config=ruff.toml] 11 | types: [python] 12 | 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v4.5.0 15 | hooks: 16 | - id: trailing-whitespace 17 | - id: end-of-file-fixer 18 | - id: check-yaml 19 | - id: check-added-large-files 20 | - id: debug-statements 21 | 22 | # Define configuration for the Python checks 23 | default_language_version: 24 | python: python3.11 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MCP-Use 2 | 3 | Thank you for your interest in contributing to MCP-Use! This document provides guidelines and instructions for contributing to this project. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Development Environment](#development-environment) 9 | - [Installation from Source](#installation-from-source) 10 | - [Development Workflow](#development-workflow) 11 | - [Branching Strategy](#branching-strategy) 12 | - [Commit Messages](#commit-messages) 13 | - [Code Style](#code-style) 14 | - [Pre-commit Hooks](#pre-commit-hooks) 15 | - [Testing](#testing) 16 | - [Running Tests](#running-tests) 17 | - [Adding Tests](#adding-tests) 18 | - [Pull Requests](#pull-requests) 19 | - [Creating a Pull Request](#creating-a-pull-request) 20 | - [Pull Request Template](#pull-request-template) 21 | - [Documentation](#documentation) 22 | - [Release Process](#release-process) 23 | - [Getting Help](#getting-help) 24 | 25 | ## Getting Started 26 | 27 | ### Development Environment 28 | 29 | MCP-Use requires: 30 | - Python 3.11 or later 31 | 32 | ### Installation from Source 33 | 34 | 1. Fork the repository on GitHub. 35 | 2. Clone your fork locally: 36 | ```bash 37 | git clone https://github.com/YOUR_USERNAME/mcp-use.git 38 | cd mcp-use 39 | ``` 40 | 3. Install the package in development mode: 41 | ```bash 42 | pip install -e ".[dev,search]" 43 | ``` 44 | 4. Set up pre-commit hooks: 45 | ```bash 46 | pip install pre-commit 47 | pre-commit install 48 | ``` 49 | 50 | ## Development Workflow 51 | 52 | ### Branching Strategy 53 | 54 | - `main` branch contains the latest stable code 55 | - Create feature branches from `main` named according to the feature you're implementing: `feature/your-feature-name` 56 | - For bug fixes, use: `fix/bug-description` 57 | 58 | ### Commit Messages 59 | 60 | For now no commit style is enforced, try to keep your commit messages informational. 61 | ### Code Style 62 | 63 | We use [Ruff](https://github.com/astral-sh/ruff) for code formatting and linting. The configuration is in `ruff.toml`. 64 | 65 | Key style guidelines: 66 | - Line length: 100 characters 67 | - Use double quotes for strings 68 | - Follow PEP 8 naming conventions 69 | - Add type hints to function signatures 70 | 71 | ### Pre-commit Hooks 72 | 73 | We use pre-commit hooks to ensure code quality before committing. The configuration is in `.pre-commit-config.yaml`. 74 | 75 | The hooks will: 76 | - Format code using Ruff 77 | - Run linting checks 78 | - Check for trailing whitespace and fix it 79 | - Ensure files end with a newline 80 | - Validate YAML files 81 | - Check for large files 82 | - Remove debug statements 83 | 84 | ## Testing 85 | 86 | ### Running Tests 87 | 88 | Run the test suite with pytest: 89 | 90 | ```bash 91 | pytest 92 | ``` 93 | 94 | To run specific test categories: 95 | 96 | ```bash 97 | pytest tests/ 98 | ``` 99 | 100 | ### Adding Tests 101 | 102 | - Add unit tests for new functionality in `tests/unit/` 103 | - For slow or network-dependent tests, mark them with `@pytest.mark.slow` or `@pytest.mark.integration` 104 | - Aim for high test coverage of new code 105 | 106 | ## Pull Requests 107 | 108 | ### Creating a Pull Request 109 | 110 | 1. Ensure your code passes all tests and pre-commit hooks 111 | 2. Push your changes to your fork 112 | 3. Submit a pull request to the main repository 113 | 4. Follow the pull request template 114 | 115 | ## Documentation 116 | 117 | - Update docstrings for new or modified functions, classes, and methods 118 | - Use Google-style docstrings: 119 | ```python 120 | def function_name(param1: type, param2: type) -> return_type: 121 | """Short description. 122 | 123 | Longer description if needed. 124 | 125 | Args: 126 | param1: Description of param1 127 | param2: Description of param2 128 | 129 | Returns: 130 | Description of return value 131 | 132 | Raises: 133 | ExceptionType: When and why this exception is raised 134 | """ 135 | ``` 136 | - Update README.md for user-facing changes 137 | 138 | ## Getting Help 139 | 140 | If you need help with your contribution: 141 | 142 | - Open an issue for discussion 143 | - Reach out to the maintainers 144 | - Check existing code for examples 145 | 146 | Thank you for contributing to MCP-Use! 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 pietrozullo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Mintlify Starter Kit 2 | 3 | Click on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including 4 | 5 | - Guide pages 6 | - Navigation 7 | - Customizations 8 | - API Reference pages 9 | - Use of popular components 10 | 11 | ### Development 12 | 13 | Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command 14 | 15 | ``` 16 | npm i -g mintlify 17 | ``` 18 | 19 | Run the following command at the root of your documentation (where docs.json is) 20 | 21 | ``` 22 | mintlify dev 23 | ``` 24 | 25 | ### Publishing Changes 26 | 27 | Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard. 28 | 29 | #### Troubleshooting 30 | 31 | - Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies. 32 | - Page loads as a 404 - Make sure you are running in a folder with `docs.json` 33 | -------------------------------------------------------------------------------- /docs/building-custom-agents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building Custom Agents 3 | description: Learn how to build custom agents using MCPClient and integrate tools with different agent frameworks 4 | --- 5 | 6 | # Building Custom Agents 7 | 8 | MCP-Use provides flexible options for building custom agents that can utilize MCP tools. This guide will show you how to create your own agents by leveraging the existing adapters, particularly focusing on the LangChain adapter. 9 | 10 | ## Overview 11 | 12 | MCP-Use allows you to: 13 | 14 | 1. Access powerful tools from MCP through connectors 15 | 2. Convert those tools to different frameworks using adapters 16 | 3. Build custom agents that utilize these tools 17 | 18 | While MCP-Use provides a built-in `MCPAgent` class, you may want to create your own custom agent implementation for more flexibility or to integrate with other frameworks. 19 | 20 | ## Using the LangChain Adapter 21 | 22 | The `LangChainAdapter` is a powerful component that converts MCP tools to LangChain tools, enabling you to use MCP tools with any LangChain-compatible agent. 23 | 24 | ### Basic Example 25 | 26 | Here's a simple example of creating a custom agent using the LangChain adapter with the simplified API: 27 | 28 | ```python 29 | import asyncio 30 | from langchain_openai import ChatOpenAI 31 | from langchain.agents import AgentExecutor, create_tool_calling_agent 32 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 33 | 34 | from mcp_use.client import MCPClient 35 | from mcp_use.adapters import LangChainAdapter 36 | 37 | async def main(): 38 | # Initialize the MCP client 39 | client = MCPClient.from_config_file("path/to/config.json") 40 | 41 | # Create adapter instance 42 | adapter = LangChainAdapter() 43 | 44 | # Get LangChain tools directly from the client with a single line 45 | tools = await adapter.create_tools(client) 46 | 47 | # Initialize your language model 48 | llm = ChatOpenAI(model="gpt-4o") 49 | 50 | # Create a prompt template 51 | prompt = ChatPromptTemplate.from_messages([ 52 | ("system", "You are a helpful assistant with access to powerful tools."), 53 | MessagesPlaceholder(variable_name="chat_history"), 54 | ("human", "{input}"), 55 | MessagesPlaceholder(variable_name="agent_scratchpad"), 56 | ]) 57 | 58 | # Create the agent 59 | agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt) 60 | 61 | # Create the agent executor 62 | agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 63 | 64 | # Run the agent 65 | result = await agent_executor.ainvoke({"input": "What can you do?"}) 66 | print(result["output"]) 67 | 68 | if __name__ == "__main__": 69 | asyncio.run(main()) 70 | ``` 71 | 72 | Note how the API simplifies tool creation - all you need is to create an adapter instance and call its `create_tools` method: 73 | ```python 74 | adapter = LangChainAdapter() 75 | tools = await adapter.create_tools(client) 76 | ``` 77 | 78 | You don't need to worry about sessions, connectors, or initialization. The adapter handles everything for you. 79 | 80 | ## Contributing New Adapters 81 | 82 | MCP-Use welcomes contributions for integrating with different agent frameworks. The adapter architecture is designed to make this process straightforward. 83 | 84 | ### Adapter Architecture 85 | 86 | MCP-Use provides a `BaseAdapter` abstract class that handles most of the common functionality: 87 | - Managing tool caching 88 | - Loading tools from connectors 89 | - Handling connector initialization 90 | - Iterating through tools from multiple connectors 91 | 92 | To create an adapter for a new framework, you only need to implement one required method: 93 | 94 | - `_convert_tool`: Convert a single MCP tool to your framework's tool format 95 | 96 | ### Creating a New Adapter 97 | 98 | Here's a simple template for creating a new adapter: 99 | 100 | ```python 101 | from typing import Any 102 | 103 | from mcp_use.adapters.base import BaseAdapter 104 | from mcp_use.connectors.base import BaseConnector 105 | from your_framework import YourFrameworkTool # Import your framework's tool class 106 | 107 | class YourFrameworkAdapter(BaseAdapter): 108 | """Adapter for converting MCP tools to YourFramework tools.""" 109 | 110 | def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> YourFrameworkTool: 111 | """Convert an MCP tool to your framework's tool format. 112 | 113 | Args: 114 | mcp_tool: The MCP tool to convert. 115 | connector: The connector that provides this tool. 116 | 117 | Returns: 118 | A tool in your framework's format, or None if conversion failed. 119 | """ 120 | try: 121 | # Implement your framework-specific conversion logic 122 | converted_tool = YourFrameworkTool( 123 | name=mcp_tool.name, 124 | description=mcp_tool.description, 125 | # Map the MCP tool properties to your framework's tool properties 126 | # You might need custom handling for argument schemas, function execution, etc. 127 | ) 128 | 129 | return converted_tool 130 | except Exception as e: 131 | self.logger.error(f"Error converting tool {mcp_tool.name}: {e}") 132 | return None 133 | ``` 134 | 135 | ### Using Your Custom Adapter 136 | 137 | Once you've implemented your adapter, you can use it with the simplified API: 138 | 139 | ```python 140 | from your_module import YourFrameworkAdapter 141 | from mcp_use.client import MCPClient 142 | 143 | # Initialize the client 144 | client = MCPClient.from_config_file("config.json") 145 | 146 | # Create an adapter instance 147 | adapter = YourFrameworkAdapter() 148 | 149 | # Get tools with a single line 150 | tools = await adapter.create_tools(client) 151 | 152 | # Use the tools with your framework 153 | agent = your_framework.create_agent(tools=tools) 154 | ``` 155 | 156 | ### Tips for Implementing an Adapter 157 | 158 | 1. **Schema Conversion**: Most frameworks have their own way of handling argument schemas. You'll need to convert the MCP tool's JSON Schema to your framework's format. 159 | 160 | 2. **Tool Execution**: When a tool is called in your framework, you'll need to pass the call to the connector's `call_tool` method and handle the result. 161 | 162 | 3. **Result Parsing**: MCP tools return structured data with types like text, images, or embedded resources. Your adapter should parse these into a format your framework understands. 163 | 164 | 4. **Error Handling**: Ensure your adapter handles errors gracefully, both during tool conversion and execution. 165 | 166 | 167 | ## Conclusion 168 | 169 | Building custom agents with MCP-Use offers tremendous flexibility while leveraging the power of MCP tools. By combining different connectors and adapters, you can create specialized agents tailored to specific tasks or integrate MCP capabilities into existing agent frameworks. 170 | 171 | The adapter architecture makes it easy to extend MCP-Use to support new frameworks - you just need to implement the `_convert_tool` method to bridge between MCP tools and your framework of choice. 172 | 173 | With the simplified API, you can create tools for your framework directly from an MCPClient by instantiating the appropriate adapter and calling its `create_tools` method, hiding all the complexity of session and connector management. 174 | 175 | We welcome contributions to expand the adapter ecosystem - if you develop an adapter for a new framework, please consider contributing it back to the project! 176 | -------------------------------------------------------------------------------- /docs/development.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Development 3 | description: "Contributing to mcp_use" 4 | --- 5 | 6 | # Development Guide 7 | 8 | This guide will help you set up your development environment and contribute to mcp_use. 9 | 10 | ## Prerequisites 11 | 12 | - Python 3.8 or higher 13 | - Git 14 | - Node.js and npm (for MCP server dependencies) 15 | 16 | ## Setting Up Development Environment 17 | 18 | 1. Clone the repository: 19 | 20 | ```bash 21 | git clone https://github.com/pietrozullo/mcp-use.git 22 | cd mcp-use 23 | ``` 24 | 25 | 2. Install development dependencies: 26 | 27 | ```bash 28 | pip install -e ".[dev]" 29 | ``` 30 | 31 | 3. Install pre-commit hooks: 32 | 33 | ```bash 34 | pre-commit install 35 | ``` 36 | 37 | ## Code Style 38 | 39 | mcp_use uses Ruff for code formatting and linting. The project follows these style guidelines: 40 | 41 | - Use type hints for all function parameters and return values 42 | - Follow PEP 8 style guide 43 | - Use docstrings for all public functions and classes 44 | - Keep functions focused and single-purpose 45 | 46 | ## Running Tests 47 | 48 | The project uses pytest for testing. To run the test suite: 49 | 50 | ```bash 51 | pytest 52 | ``` 53 | 54 | For more specific test runs: 55 | 56 | ```bash 57 | # Run tests with coverage 58 | pytest --cov=mcp_use 59 | 60 | # Run specific test file 61 | pytest tests/test_client.py 62 | 63 | # Run tests with verbose output 64 | pytest -v 65 | ``` 66 | 67 | ## Documentation 68 | 69 | Documentation is written in MDX format and uses Mintlify for rendering. To preview documentation changes: 70 | 71 | 1. Install Mintlify CLI: 72 | 73 | ```bash 74 | npm i -g mintlify 75 | ``` 76 | 77 | 2. Run the development server: 78 | 79 | ```bash 80 | mintlify dev 81 | ``` 82 | 83 | ## Contributing 84 | 85 | 1. Create a new branch for your feature: 86 | 87 | ```bash 88 | git checkout -b feature/your-feature-name 89 | ``` 90 | 91 | 2. Make your changes and commit them: 92 | 93 | ```bash 94 | git add . 95 | git commit -m "Description of your changes" 96 | ``` 97 | 98 | 3. Push your changes and create a pull request: 99 | 100 | ```bash 101 | git push origin feature/your-feature-name 102 | ``` 103 | 104 | ## Project Structure 105 | 106 | ``` 107 | mcp-use/ 108 | ├── mcp_use/ # Main package code 109 | ├── tests/ # Test files 110 | ├── examples/ # Example usage 111 | ├── docs/ # Documentation 112 | ├── static/ # Static assets 113 | └── pyproject.toml # Project configuration 114 | ``` 115 | 116 | ## Adding New MCP Servers 117 | 118 | To add support for a new MCP server: 119 | 120 | 1. Create a new configuration template in the examples directory 121 | 2. Add necessary server-specific code in the `mcp_use` package 122 | 3. Update documentation with new server information 123 | 4. Add tests for the new server functionality 124 | 125 | ## Release Process 126 | 127 | 1. Update version in `pyproject.toml` 128 | 2. Update CHANGELOG.md 129 | 3. Create a new release tag 130 | 4. Build and publish to PyPI: 131 | 132 | ```bash 133 | python -m build 134 | python -m twine upload dist/* 135 | ``` 136 | -------------------------------------------------------------------------------- /docs/docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://mintlify.com/docs.json", 3 | "theme": "mint", 4 | "name": "mcp_use", 5 | "colors": { 6 | "primary": "#062c24", 7 | "light": "#55e4c8", 8 | "dark": "#000000" 9 | }, 10 | "favicon": "/favicon.svg", 11 | "navigation": { 12 | "tabs": [ 13 | { 14 | "tab": "Documentation", 15 | "groups": [ 16 | { 17 | "group": "Getting Started", 18 | "pages": [ 19 | "introduction", 20 | "quickstart" 21 | ] 22 | }, 23 | { 24 | "group": "Essentials", 25 | "pages": [ 26 | "essentials/configuration", 27 | "essentials/llm-integration", 28 | "essentials/debugging", 29 | "essentials/connection-types", 30 | "essentials/server-manager", 31 | "building-custom-agents" 32 | ] 33 | }, 34 | { 35 | "group": "API Reference", 36 | "pages": [ 37 | "api-reference/introduction" 38 | ] 39 | }, 40 | { 41 | "group": "Development", 42 | "pages": [ 43 | "development" 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "logo": { 51 | "light": "/logo/light.svg", 52 | "dark": "/logo/dark.svg" 53 | }, 54 | "navbar": { 55 | "links": [ 56 | { 57 | "label": "GitHub", 58 | "href": "https://github.com/pietrozullo/mcp-use" 59 | } 60 | ] 61 | }, 62 | "footer": { 63 | "socials": { 64 | "github": "https://github.com/pietrozullo/mcp-use" 65 | } 66 | }, 67 | "anchors": [ 68 | { 69 | "name": "Documentation", 70 | "icon": "book-open", 71 | "url": "/" 72 | }, 73 | { 74 | "name": "API Reference", 75 | "icon": "code", 76 | "url": "/api-reference" 77 | }, 78 | { 79 | "name": "Development", 80 | "icon": "code-branch", 81 | "url": "/development" 82 | } 83 | ], 84 | "feedback": { 85 | "suggestEdit": true, 86 | "raiseIssue": true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/essentials/configuration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | description: "Configure your mcp_use environment" 4 | --- 5 | 6 | # Configuration Guide 7 | 8 | This guide covers all the configuration options available in mcp_use. 9 | 10 | ## API Keys 11 | 12 | Make sure to have the api key relative to the provider of your choice available in the environment you can either: 13 | 14 | 1 - Create `.env` file with your keys as: 15 | 16 | ```bash 17 | # OpenAI 18 | OPENAI_API_KEY=your_api_key_here 19 | # Anthropic 20 | ANTHROPIC_API_KEY=your_api_key_here 21 | # Groq 22 | GROQ_API_KEY=your_api_key_here 23 | ``` 24 | 25 | and load it in python using 26 | 27 | ```python 28 | from dotenv import load_dotenv 29 | load_dotenv() 30 | ``` 31 | 32 | this will make all the keys defibned in `.env` available in yout python runtime, granted that you run from where the .env is located. 33 | 34 | 2 - Set it in your environment by running in your terminal the following command, e.g. for openai: 35 | 36 | ```bash 37 | export OPENAI_API_KEY='..." 38 | ``` 39 | 40 | and then import it in your python code as: 41 | 42 | ```python 43 | import os 44 | OPENAI_API_KEY = os.getenv(OPENAI_API_KEY,"") 45 | ``` 46 | 47 | or any other method you might prefer. 48 | 49 | ## MCP Server Configuration 50 | 51 | mcp_use supports any MCP server through a flexible configuration system. (For a list of awesome servers you can visit https://github.com/punkpeye/awesome-mcp-servers or https://github.com/appcypher/awesome-mcp-servers which have an amazing collection of them) 52 | 53 | The configuration is defined in a JSON file with the following structure: 54 | 55 | ```json 56 | { 57 | "mcpServers": { 58 | "server_name": { 59 | "command": "command_to_run", 60 | "args": ["arg1", "arg2"], 61 | "env": { 62 | "ENV_VAR": "value" 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | MCP servers can use different connection types (STDIO, HTTP). For details on these connection types and how to configure them, see the [Connection Types](./connection-types) guide. 70 | 71 | ### Configuration Options 72 | 73 | - `server_name`: A unique identifier for your MCP server 74 | - `command`: The command to start the MCP server 75 | - `args`: Array of arguments to pass to the command 76 | - `env`: Environment variables to set for the server 77 | 78 | ### Example Configuration 79 | 80 | Here's a basic example of how to configure an MCP server: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "my_server": { 86 | "command": "npx", 87 | "args": ["@my-mcp/server"], 88 | "env": { 89 | "PORT": "3000" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | ### Multiple Server Configuration 97 | 98 | You can configure multiple MCP servers in a single configuration file, allowing you to use different servers for different tasks or combine their capabilities (e.g.): 99 | 100 | ```json 101 | { 102 | "mcpServers": { 103 | "airbnb": { 104 | "command": "npx", 105 | "args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"] 106 | }, 107 | "playwright": { 108 | "command": "npx", 109 | "args": ["@playwright/mcp@latest"], 110 | "env": { "DISPLAY": ":1" } 111 | }, 112 | "filesystem": { 113 | "command": "npx", 114 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/pietro/projects/mcp-use/"] 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | For a complete example of using multiple servers, see the [multi-server example](https://github.com/pietrozullo/mcp-use/blob/main/examples/multi_server_example.py) in our repository. 121 | 122 | ## Agent Configuration 123 | 124 | When creating an MCPAgent, you can configure several parameters: 125 | 126 | ```python 127 | from mcp_use import MCPAgent, MCPClient 128 | from langchain_openai import ChatOpenAI 129 | 130 | # Basic configuration 131 | agent = MCPAgent( 132 | llm=ChatOpenAI(model="gpt-4o", temperature=0.7), 133 | client=MCPClient.from_config_file("config.json"), 134 | max_steps=30 135 | ) 136 | 137 | # Advanced configuration 138 | agent = MCPAgent( 139 | llm=ChatOpenAI(model="gpt-4o", temperature=0.7), 140 | client=MCPClient.from_config_file("config.json"), 141 | max_steps=30, 142 | server_name=None, 143 | auto_initialize=True, 144 | memory_enabled=True, 145 | system_prompt="Custom instructions for the agent", 146 | additional_instructions="Additional guidelines for specific tasks", 147 | disallowed_tools=["file_system", "network", "shell"] # Restrict potentially dangerous tools 148 | ) 149 | ``` 150 | 151 | ### Available Parameters 152 | 153 | - `llm`: Any LangChain-compatible language model (required) 154 | - `client`: The MCPClient instance (optional if connectors are provided) 155 | - `connectors`: List of connectors if not using client (optional) 156 | - `server_name`: Name of the server to use (optional) 157 | - `max_steps`: Maximum number of steps the agent can take (default: 5) 158 | - `auto_initialize`: Whether to initialize automatically (default: False) 159 | - `memory_enabled`: Whether to enable memory (default: True) 160 | - `system_prompt`: Custom system prompt (optional) 161 | - `system_prompt_template`: Custom system prompt template (optional) 162 | - `additional_instructions`: Additional instructions for the agent (optional) 163 | - `disallowed_tools`: List of tool names that should not be available to the agent (optional) 164 | 165 | ### Tool Access Control 166 | 167 | You can restrict which tools are available to the agent for security or to limit its capabilities: 168 | 169 | ```python 170 | # Create agent with restricted tools 171 | agent = MCPAgent( 172 | llm=ChatOpenAI(model="gpt-4o"), 173 | client=client, 174 | disallowed_tools=["file_system", "network", "shell"] # Restrict potentially dangerous tools 175 | ) 176 | 177 | # Update restrictions after initialization 178 | agent.set_disallowed_tools(["file_system", "network", "shell", "database"]) 179 | await agent.initialize() # Reinitialize to apply changes 180 | 181 | # Check current restrictions 182 | restricted_tools = agent.get_disallowed_tools() 183 | print(f"Restricted tools: {restricted_tools}") 184 | ``` 185 | 186 | This feature is useful for: 187 | 188 | - Restricting access to sensitive operations 189 | - Limiting agent capabilities for specific tasks 190 | - Preventing the agent from using potentially dangerous tools 191 | - Focusing the agent on specific functionality 192 | 193 | ## Error Handling 194 | 195 | mcp_use provides several ways to handle errors: 196 | 197 | 1. **Connection Errors**: Check your MCP server configuration and ensure the server is running 198 | 2. **Authentication Errors**: Verify your API keys are correctly set in the environment 199 | 3. **Timeout Errors**: Adjust the `max_steps` parameter if operations are timing out 200 | 201 | ## Best Practices 202 | 203 | 1. Always use environment variables for sensitive information 204 | 2. Keep configuration files in version control (without sensitive data) 205 | 3. Use appropriate timeouts for different types of operations 206 | 4. Enable verbose logging during development 207 | 5. Test configurations in a development environment before production 208 | -------------------------------------------------------------------------------- /docs/essentials/connection-types.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Connection Types 3 | description: "Understanding the different connection types for MCP servers" 4 | --- 5 | 6 | # Connection Types for MCP Servers 7 | 8 | MCP servers can communicate with clients using different connection protocols, each with its own advantages and use cases. This guide explains the three primary connection types supported by mcp_use: 9 | 10 | ## Standard Input/Output (STDIO) 11 | 12 | STDIO connections run the MCP server as a child process and communicate through standard input and output streams. 13 | 14 | ### Characteristics: 15 | 16 | - **Local Operation**: The server runs as a child process on the same machine 17 | - **Simplicity**: Easy to set up with minimal configuration 18 | - **Security**: No network exposure, ideal for sensitive operations 19 | - **Performance**: Low latency for local operations 20 | 21 | ### Configuration Example: 22 | 23 | ```json 24 | { 25 | "mcpServers": { 26 | "stdio_server": { 27 | "command": "npx", 28 | "args": ["@my-mcp/server"], 29 | "env": {} 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## HTTP Connections 36 | 37 | HTTP connections communicate with MCP servers over standard HTTP/HTTPS protocols. 38 | 39 | ### Characteristics: 40 | 41 | - **RESTful Architecture**: Follows familiar HTTP request/response patterns 42 | - **Statelessness**: Each request is independent 43 | - **Compatibility**: Works well with existing web infrastructure 44 | - **Firewall-Friendly**: Uses standard ports that are typically open 45 | 46 | ### Configuration Example: 47 | 48 | ```json 49 | { 50 | "mcpServers": { 51 | "http_server": { 52 | "url": "http://localhost:3000", 53 | "headers": { 54 | "Authorization": "Bearer ${AUTH_TOKEN}" 55 | } 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | ## Choosing the Right Connection Type 62 | 63 | The choice of connection type depends on your specific use case: 64 | 65 | 1. **STDIO**: Best for local development, testing, and enhanced security scenarios where network exposure is a concern 66 | 67 | 2. **HTTP**: Ideal for stateless operations, simple integrations, and when working with existing HTTP infrastructure 68 | 69 | When configuring your mcp_use environment, you can specify the connection type in your configuration file as shown in the examples above. 70 | 71 | ## Using Connection Types 72 | 73 | Connection types are automatically inferred from your configuration file based on the parameters provided: 74 | 75 | ```python 76 | from mcp_use import MCPClient 77 | 78 | # The connection type is automatically inferred based on your config file 79 | client = MCPClient.from_config_file("config.json", server_name="my_server") 80 | ``` 81 | 82 | For example: 83 | 84 | - If your configuration includes `command` and `args`, a STDIO connection will be used 85 | - If your configuration has a `url` starting with `http://` or `https://`, an HTTP connection will be used 86 | 87 | This automatic inference simplifies the configuration process and ensures the appropriate connection type is used without requiring explicit specification. 88 | 89 | For more details on connection configuration, see the [Configuration Guide](./configuration). 90 | -------------------------------------------------------------------------------- /docs/essentials/debugging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Debugging' 3 | description: 'Learn how to debug and log in mcp-use' 4 | --- 5 | 6 | # Debugging MCP-Use 7 | 8 | MCP-Use provides built-in debugging functionality that increases log verbosity and helps diagnose issues in your agent implementation. 9 | 10 | ## Enabling Debug Mode 11 | 12 | There are two primary ways to enable debug mode: 13 | 14 | ### 1. Environment Variable (Recommended for One-off Runs) 15 | 16 | Run your script with the `DEBUG` environment variable set to the desired level: 17 | 18 | ```bash 19 | # Level 1: Show INFO level messages 20 | DEBUG=1 python3.11 examples/browser_use.py 21 | 22 | # Level 2: Show DEBUG level messages (full verbose output) 23 | DEBUG=2 python3.11 examples/browser_use.py 24 | ``` 25 | 26 | This sets the debug level only for the duration of that specific Python process. This is particularly useful for quickly troubleshooting issues without modifying your code. 27 | Alternatively you can set the environment variable MCP_USE_DEBUG as such: 28 | ```bash 29 | export MCP_USE_DEBUG=1 # or 2 30 | ``` 31 | 32 | ### 2. Setting the Debug Flag Programmatically 33 | 34 | You can set the global debug flag directly in your code, which is useful for debugging specific parts of your application or conditionally enabling debug mode based on your application state: 35 | 36 | ```python 37 | import mcp_use 38 | 39 | mcp_use.set_debug(1) # INFO level 40 | # or 41 | mcp_use.set_debug(2) # DEBUG level (full verbose output) 42 | # or 43 | mcp_use.set_debug(0) # Turn off debug (WARNING level) 44 | ``` 45 | 46 | ## Debug Levels 47 | 48 | MCP-Use supports different levels of debugging: 49 | 50 | | Level | Environment Variable | Program Setting | Description | 51 | |-------|---------------------|-----------------|-------------| 52 | | 0 | (not set) | `set_debug(0)` | Normal operation, only WARNING and above messages are shown | 53 | | 1 | `DEBUG=1` | `set_debug(1)` | INFO level messages are shown - useful for basic operational information. Shows tool calls.| 54 | | 2 | `DEBUG=2` | `set_debug(2)` | Full DEBUG level - all detailed debugging information is shown | 55 | 56 | ## Agent-Specific Verbosity 57 | 58 | If you only want to increase verbosity for the agent component without enabling full debug mode for the entire package, you can use the `verbose` parameter when creating an MCPAgent: 59 | 60 | ```python 61 | from mcp_use import MCPAgent 62 | 63 | # Create agent with increased verbosity 64 | agent = MCPAgent( 65 | llm=your_llm, 66 | client=your_client, 67 | verbose=True # Only shows debug messages from the agent 68 | ) 69 | ``` 70 | 71 | This option is useful when you want to see the agent's steps and decision-making process without all the low-level debug information from other components. 72 | 73 | ## Debug Information 74 | 75 | When debug mode is enabled, you'll see more detailed information about: 76 | 77 | - Server initialization and connection details 78 | - Tool registration and resolution 79 | - Agent steps and decision-making 80 | - Request and response formats 81 | - Communication with MCP servers 82 | - Error details and stack traces 83 | 84 | This can be extremely helpful when diagnosing issues with custom MCP servers or understanding why an agent might be behaving unexpectedly. 85 | 86 | ## Langsmith 87 | 88 | Langchain offers a very nice tool to debug agent behaviour which integrates seamlessly with mcp-use. You can visit https://smith.langchain.com/ and login, they will give you a set of variables to copy in an .env file 89 | you will be then able to visualize the agent behaviour on their platform. 90 | 91 | ## Troubleshooting Common Issues 92 | 93 | ### Server Connection Problems 94 | 95 | If you're having issues connecting to MCP servers, enabling debug mode will show detailed information about the connection attempts, server initialization, and any errors encountered. 96 | 97 | ### Agent Not Using Expected Tools 98 | 99 | When debug mode is enabled, you'll see each tool registration and the exact prompts being sent to the LLM, which can help diagnose why certain tools might not be used as expected. 100 | 101 | ### Performance Issues 102 | 103 | Debug logs can help identify potential bottlenecks in your implementation by showing timing information for various operations. 104 | -------------------------------------------------------------------------------- /docs/essentials/llm-integration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: LLM Integration 3 | description: "Integrate any LLM with mcp_use through LangChain" 4 | --- 5 | 6 | # LLM Integration Guide 7 | 8 | mcp_use supports integration with **any** Language Learning Model (LLM) that is compatible with LangChain. This guide covers how to use different LLM providers with mcp_use and emphasizes the flexibility to use any LangChain-supported model. 9 | 10 | ## Universal LLM Support 11 | 12 | mcp_use leverages LangChain's architecture to support any LLM that implements the LangChain interface. This means you can use virtually any model from any provider, including: 13 | 14 | - OpenAI models (GPT-4, GPT-3.5, etc.) 15 | - Anthropic models (Claude) 16 | - Google models (Gemini) 17 | - Mistral models 18 | - Groq models 19 | - Llama models 20 | - Cohere models 21 | - Open source models (via LlamaCpp, HuggingFace, etc.) 22 | - Custom or self-hosted models 23 | - Any other model with a LangChain integration 24 | 25 | 26 | Read more at https://python.langchain.com/docs/integrations/chat/ 27 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/images/hero-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pietrozullo/mcp-use/dddbf5231ea28f83b95bf1a9da577067d4a83963/docs/images/hero-dark.png -------------------------------------------------------------------------------- /docs/images/hero-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pietrozullo/mcp-use/dddbf5231ea28f83b95bf1a9da577067d4a83963/docs/images/hero-light.png -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: "Welcome to mcp_use - The Open Source MCP Client Library" 4 | --- 5 | 6 | mcp_use Light Theme 7 | mcp_use Dark Theme 8 | 9 | ## What is mcp_use? 10 | 11 | mcp_use is an open source library that enables developers to connect any Language Learning Model (LLM) to any MCP server, allowing the creation of custom agents with tool access without relying on closed-source or application-specific clients. 12 | 13 | ## Key Features 14 | 15 | 16 | 17 | Connect any LLM to any MCP server without vendor lock-in 18 | 19 | 20 | Support for any MCP server through a simple configuration system 21 | 22 | 23 | Simple JSON-based configuration for MCP server integration 24 | 25 | 26 | Compatible with any LangChain-supported LLM provider 27 | 28 | 29 | Connect to MCP servers running on specific HTTP ports for web-based integrations 30 | 31 | 32 | Agents can dynamically choose the most appropriate MCP server for the task. 33 | 34 | 35 | 36 | ## Getting Started 37 | 38 | 39 | 40 | Install mcp_use and set up your environment 41 | 42 | 43 | Learn how to configure mcp_use with your MCP server 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/logo/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/logo/light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | description: "Get started with mcp_use in minutes" 4 | --- 5 | 6 | # Quickstart Guide 7 | 8 | This guide will help you get started with mcp_use quickly. We'll cover installation, basic configuration, and running your first agent. 9 | 10 | ## Installation 11 | 12 | You can install mcp_use using pip: 13 | 14 | ```bash 15 | pip install mcp-use 16 | ``` 17 | 18 | Or install from source: 19 | 20 | ```bash 21 | git clone https://github.com/pietrozullo/mcp-use.git 22 | cd mcp-use 23 | pip install -e . 24 | ``` 25 | 26 | ## Installing LangChain Providers 27 | 28 | mcp_use works with various LLM providers through LangChain. You'll need to install the appropriate LangChain provider package for your chosen LLM. For example: 29 | 30 | ```bash 31 | # For OpenAI 32 | pip install langchain-openai 33 | 34 | # For Anthropic 35 | pip install langchain-anthropic 36 | 37 | # For other providers, check the [LangChain chat models documentation](https://python.langchain.com/docs/integrations/chat/) 38 | ``` 39 | 40 | > **Important**: Only models with tool calling capabilities can be used with mcp_use. Make sure your chosen model supports function calling or tool use. 41 | 42 | ## Environment Setup 43 | 44 | Set up your environment variables in a `.env` file: 45 | 46 | ```bash 47 | OPENAI_API_KEY=your_api_key_here 48 | ANTHROPIC_API_KEY=your_api_key_here 49 | ``` 50 | 51 | ## Your First Agent 52 | 53 | Here's a simple example to get you started: 54 | 55 | ```python 56 | import asyncio 57 | import os 58 | from dotenv import load_dotenv 59 | from langchain_openai import ChatOpenAI 60 | from mcp_use import MCPAgent, MCPClient 61 | 62 | async def main(): 63 | # Load environment variables 64 | load_dotenv() 65 | 66 | # Create configuration dictionary 67 | config = { 68 | "mcpServers": { 69 | "playwright": { 70 | "command": "npx", 71 | "args": ["@playwright/mcp@latest"], 72 | "env": { 73 | "DISPLAY": ":1" 74 | } 75 | } 76 | } 77 | } 78 | 79 | # Create MCPClient from configuration dictionary 80 | client = MCPClient.from_dict(config) 81 | 82 | # Create LLM 83 | llm = ChatOpenAI(model="gpt-4o") 84 | 85 | # Create agent with the client 86 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 87 | 88 | # Run the query 89 | result = await agent.run( 90 | "Find the best restaurant in San Francisco USING GOOGLE SEARCH", 91 | ) 92 | print(f"\nResult: {result}") 93 | 94 | if __name__ == "__main__": 95 | asyncio.run(main()) 96 | ``` 97 | 98 | ## Configuration Options 99 | 100 | You can also add the servers configuration from a config file: 101 | 102 | ```python 103 | client = MCPClient.from_config_file( 104 | os.path.join("browser_mcp.json") 105 | ) 106 | ``` 107 | 108 | Example configuration file (`browser_mcp.json`): 109 | 110 | ```json 111 | { 112 | "mcpServers": { 113 | "playwright": { 114 | "command": "npx", 115 | "args": ["@playwright/mcp@latest"], 116 | "env": { 117 | "DISPLAY": ":1" 118 | } 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## Working with Adapters Directly 125 | 126 | If you want more control over how tools are created, you can work with the adapters directly. The `BaseAdapter` class provides a unified interface for converting MCP tools to various framework formats, with `LangChainAdapter` being the most commonly used implementation. 127 | 128 | ```python 129 | import asyncio 130 | from langchain_openai import ChatOpenAI 131 | from langchain.agents import AgentExecutor, create_tool_calling_agent 132 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 133 | 134 | from mcp_use.client import MCPClient 135 | from mcp_use.adapters import LangChainAdapter 136 | 137 | async def main(): 138 | # Initialize client 139 | client = MCPClient.from_config_file("browser_mcp.json") 140 | 141 | # Create an adapter instance 142 | adapter = LangChainAdapter() 143 | 144 | # Get tools directly from the client 145 | tools = await adapter.create_tools(client) 146 | 147 | # Use the tools with any LangChain agent 148 | llm = ChatOpenAI(model="gpt-4o") 149 | prompt = ChatPromptTemplate.from_messages([ 150 | ("system", "You are a helpful assistant with access to powerful tools."), 151 | MessagesPlaceholder(variable_name="chat_history"), 152 | ("human", "{input}"), 153 | MessagesPlaceholder(variable_name="agent_scratchpad"), 154 | ]) 155 | 156 | agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt) 157 | agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 158 | 159 | result = await agent_executor.ainvoke({"input": "Search for information about climate change"}) 160 | print(result["output"]) 161 | 162 | if __name__ == "__main__": 163 | asyncio.run(main()) 164 | ``` 165 | 166 | The adapter pattern makes it easy to: 167 | 168 | 1. Create tools directly from an MCPClient 169 | 2. Filter or customize which tools are available 170 | 3. Integrate with different agent frameworks 171 | 172 | ## Using Multiple Servers 173 | 174 | The `MCPClient` can be configured with multiple MCP servers, allowing your agent to access tools from different sources. This capability enables complex workflows spanning various domains (e.g., web browsing and API interaction). 175 | 176 | **Configuration:** 177 | 178 | Define multiple servers in your configuration file (`multi_server_config.json`): 179 | 180 | ```json 181 | { 182 | "mcpServers": { 183 | "airbnb": { 184 | "command": "npx", 185 | "args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"] 186 | }, 187 | "playwright": { 188 | "command": "npx", 189 | "args": ["@playwright/mcp@latest"], 190 | "env": { 191 | "DISPLAY": ":1" 192 | } 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | **Usage:** 199 | 200 | When an `MCPClient` with multiple servers is passed to an `MCPAgent`, the agent gains access to tools from all configured servers. By default, you might need to guide the agent or explicitly specify which server to use for a given task using the `server_name` parameter in the `agent.run()` method. 201 | 202 | ```python 203 | # Assuming MCPClient is initialized with the multi_server_config.json 204 | client = MCPClient.from_config_file("multi_server_config.json") 205 | agent = MCPAgent(llm=llm, client=client) # Server manager not enabled by default 206 | 207 | # Manually specify the server if needed 208 | result = await agent.run( 209 | "Search for Airbnb listings in Barcelona", 210 | server_name="airbnb" 211 | ) 212 | ``` 213 | 214 | ## Enabling Dynamic Server Selection (Server Manager) 215 | 216 | To improve efficiency and potentially reduce agent confusion when many tools are available, you can enable the Server Manager. Set `use_server_manager=True` when creating the `MCPAgent`. 217 | 218 | When enabled, the agent will automatically select the appropriate server based on the tool chosen by the LLM for each step. This avoids connecting to unnecessary servers. 219 | 220 | ```python 221 | # Assuming MCPClient is initialized with the multi_server_config.json 222 | client = MCPClient.from_config_file("multi_server_config.json") 223 | agent = MCPAgent(llm=llm, client=client, use_server_manager=True) # Enable server manager 224 | 225 | # The agent can now use tools from both airbnb and playwright servers 226 | result = await agent.run( 227 | "Search for a place in Barcelona on Airbnb, then Google nearby restaurants." 228 | ) 229 | ``` 230 | 231 | ## Restricting Tool Access 232 | 233 | You can control which tools are available to the agent: 234 | 235 | ```python 236 | import asyncio 237 | import os 238 | from dotenv import load_dotenv 239 | from langchain_openai import ChatOpenAI 240 | from mcp_use import MCPAgent, MCPClient 241 | 242 | async def main(): 243 | # Load environment variables 244 | load_dotenv() 245 | 246 | # Create configuration dictionary 247 | config = { 248 | "mcpServers": { 249 | "playwright": { 250 | "command": "npx", 251 | "args": ["@playwright/mcp@latest"], 252 | "env": { 253 | "DISPLAY": ":1" 254 | } 255 | } 256 | } 257 | } 258 | 259 | # Create MCPClient from configuration dictionary 260 | client = MCPClient.from_dict(config) 261 | 262 | # Create LLM 263 | llm = ChatOpenAI(model="gpt-4o") 264 | 265 | # Create agent with restricted tools 266 | agent = MCPAgent( 267 | llm=llm, 268 | client=client, 269 | max_steps=30, 270 | disallowed_tools=["file_system", "network"] # Restrict potentially dangerous tools 271 | ) 272 | 273 | # Run the query 274 | result = await agent.run( 275 | "Find the best restaurant in San Francisco USING GOOGLE SEARCH", 276 | ) 277 | print(f"\nResult: {result}") 278 | 279 | if __name__ == "__main__": 280 | asyncio.run(main()) 281 | ``` 282 | 283 | ## Available MCP Servers 284 | 285 | mcp_use supports any MCP server, allowing you to connect to a wide range of server implementations. For a comprehensive list of available servers, check out the [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers) repository. 286 | 287 | Each server requires its own configuration. Check the [Configuration Guide](/essentials/configuration) for details. 288 | 289 | ## HTTP Connection 290 | 291 | mcp_use now supports HTTP connections, allowing you to connect to MCP servers running on specific HTTP ports. This feature is particularly useful for integrating with web-based MCP servers. 292 | 293 | Here's a simple example to get you started with HTTP connections: 294 | 295 | ```python 296 | import asyncio 297 | import os 298 | from dotenv import load_dotenv 299 | from langchain_openai import ChatOpenAI 300 | from mcp_use import MCPAgent, MCPClient 301 | 302 | async def main(): 303 | # Load environment variables 304 | load_dotenv() 305 | 306 | # Create configuration dictionary 307 | config = { 308 | "mcpServers": { 309 | "http": { 310 | "url": "http://localhost:8931/sse" 311 | } 312 | } 313 | } 314 | 315 | # Create MCPClient from configuration dictionary 316 | client = MCPClient.from_dict(config) 317 | 318 | # Create LLM 319 | llm = ChatOpenAI(model="gpt-4o") 320 | 321 | # Create agent with the client 322 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 323 | 324 | # Run the query 325 | result = await agent.run( 326 | "Find the best restaurant in San Francisco USING GOOGLE SEARCH", 327 | ) 328 | print(f"\nResult: {result}") 329 | 330 | if __name__ == "__main__": 331 | asyncio.run(main()) 332 | ``` 333 | 334 | This example demonstrates how to connect to an MCP server running on a specific HTTP port. Make sure to start your MCP server before running this example. 335 | 336 | ## Next Steps 337 | 338 | - Learn about [Configuration Options](/essentials/configuration) 339 | - Explore [Example Use Cases](/examples) 340 | - Check out [Advanced Features](/essentials/advanced) 341 | -------------------------------------------------------------------------------- /docs/snippets/snippet-intro.mdx: -------------------------------------------------------------------------------- 1 | One of the core principles of software development is DRY (Don't Repeat 2 | Yourself). This is a principle that apply to documentation as 3 | well. If you find yourself repeating the same content in multiple places, you 4 | should consider creating a custom snippet to keep your content in sync. 5 | -------------------------------------------------------------------------------- /examples/airbnb_mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "airbnb": { 4 | "command": "npx", 5 | "args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/airbnb_use.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example demonstrating how to use mcp_use with Airbnb. 3 | 4 | This example shows how to connect an LLM to Airbnb through MCP tools 5 | to perform tasks like searching for accommodations. 6 | 7 | Special Thanks to https://github.com/openbnb-org/mcp-server-airbnb for the server. 8 | """ 9 | 10 | import asyncio 11 | import os 12 | 13 | from dotenv import load_dotenv 14 | from langchain_anthropic import ChatAnthropic 15 | 16 | from mcp_use import MCPAgent, MCPClient 17 | 18 | 19 | async def run_airbnb_example(): 20 | """Run an example using Airbnb MCP server.""" 21 | # Load environment variables 22 | load_dotenv() 23 | 24 | # Create MCPClient with Airbnb configuration 25 | client = MCPClient.from_config_file(os.path.join(os.path.dirname(__file__), "airbnb_mcp.json")) 26 | # Create LLM - you can choose between different models 27 | llm = ChatAnthropic(model="claude-3-5-sonnet-20240620") 28 | # Alternative models: 29 | # llm = init_chat_model(model="llama-3.1-8b-instant", model_provider="groq") 30 | # llm = ChatOpenAI(model="gpt-4o") 31 | 32 | # Create agent with the client 33 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 34 | 35 | try: 36 | # Run a query to search for accommodations 37 | result = await agent.run( 38 | "Find me a nice place to stay in Barcelona for 2 adults " 39 | "for a week in August. I prefer places with a pool and " 40 | "good reviews. Show me the top 3 options.", 41 | max_steps=30, 42 | ) 43 | print(f"\nResult: {result}") 44 | finally: 45 | # Ensure we clean up resources properly 46 | if client.sessions: 47 | await client.close_all_sessions() 48 | 49 | 50 | if __name__ == "__main__": 51 | asyncio.run(run_airbnb_example()) 52 | -------------------------------------------------------------------------------- /examples/blender_use.py: -------------------------------------------------------------------------------- 1 | """ 2 | Blender MCP example for mcp_use. 3 | 4 | This example demonstrates how to use the mcp_use library with MCPClient 5 | to connect an LLM to Blender through MCP tools via WebSocket. 6 | The example assumes you have installed the Blender MCP addon from: 7 | https://github.com/ahujasid/blender-mcp 8 | 9 | Make sure the addon is enabled in Blender preferences and the WebSocket 10 | server is running before executing this script. 11 | 12 | Special thanks to https://github.com/ahujasid/blender-mcp for the server. 13 | """ 14 | 15 | import asyncio 16 | 17 | from dotenv import load_dotenv 18 | from langchain_anthropic import ChatAnthropic 19 | 20 | from mcp_use import MCPAgent, MCPClient 21 | 22 | 23 | async def run_blender_example(): 24 | """Run the Blender MCP example.""" 25 | # Load environment variables 26 | load_dotenv() 27 | 28 | # Create MCPClient with Blender MCP configuration 29 | config = {"mcpServers": {"blender": {"command": "uvx", "args": ["blender-mcp"]}}} 30 | client = MCPClient.from_dict(config) 31 | 32 | # Create LLM 33 | llm = ChatAnthropic(model="claude-3-5-sonnet-20240620") 34 | 35 | # Create agent with the client 36 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 37 | 38 | try: 39 | # Run the query 40 | result = await agent.run( 41 | "Create an inflatable cube with soft material and a plane as ground.", 42 | max_steps=30, 43 | ) 44 | print(f"\nResult: {result}") 45 | finally: 46 | # Ensure we clean up resources properly 47 | if client.sessions: 48 | await client.close_all_sessions() 49 | 50 | 51 | if __name__ == "__main__": 52 | # Run the Blender example 53 | asyncio.run(run_blender_example()) 54 | -------------------------------------------------------------------------------- /examples/browser_mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "playwright": { 4 | "command": "npx", 5 | "args": ["@playwright/mcp@latest"], 6 | "env": { 7 | "DISPLAY": ":1" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/browser_use.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic usage example for mcp_use. 3 | 4 | This example demonstrates how to use the mcp_use library with MCPClient 5 | to connect any LLM to MCP tools through a unified interface. 6 | 7 | Special thanks to https://github.com/microsoft/playwright-mcp for the server. 8 | """ 9 | 10 | import asyncio 11 | import os 12 | 13 | from dotenv import load_dotenv 14 | from langchain_openai import ChatOpenAI 15 | 16 | from mcp_use import MCPAgent, MCPClient 17 | 18 | 19 | async def main(): 20 | """Run the example using a configuration file.""" 21 | # Load environment variables 22 | load_dotenv() 23 | 24 | # Create MCPClient from config file 25 | client = MCPClient.from_config_file(os.path.join(os.path.dirname(__file__), "browser_mcp.json")) 26 | 27 | # Create LLM 28 | llm = ChatOpenAI(model="gpt-4o") 29 | # llm = init_chat_model(model="llama-3.1-8b-instant", model_provider="groq") 30 | # llm = ChatAnthropic(model="claude-3-") 31 | # llm = ChatGroq(model="llama3-8b-8192") 32 | 33 | # Create agent with the client 34 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 35 | 36 | # Run the query 37 | result = await agent.run( 38 | """ 39 | Navigate to https://github.com/mcp-use/mcp-use, give a star to the project and write 40 | a summary of the project. 41 | """, 42 | max_steps=30, 43 | ) 44 | print(f"\nResult: {result}") 45 | 46 | 47 | if __name__ == "__main__": 48 | # Run the appropriate example 49 | asyncio.run(main()) 50 | -------------------------------------------------------------------------------- /examples/chat_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple chat example using MCPAgent with built-in conversation memory. 3 | 4 | This example demonstrates how to use the MCPAgent with its built-in 5 | conversation history capabilities for better contextual interactions. 6 | 7 | Special thanks to https://github.com/microsoft/playwright-mcp for the server. 8 | """ 9 | 10 | import asyncio 11 | 12 | from dotenv import load_dotenv 13 | from langchain_openai import ChatOpenAI 14 | 15 | from mcp_use import MCPAgent, MCPClient 16 | 17 | 18 | async def run_memory_chat(): 19 | """Run a chat using MCPAgent's built-in conversation memory.""" 20 | # Load environment variables for API keys 21 | load_dotenv() 22 | 23 | # Config file path - change this to your config file 24 | config_file = "examples/browser_mcp.json" 25 | 26 | print("Initializing chat...") 27 | 28 | # Create MCP client and agent with memory enabled 29 | client = MCPClient.from_config_file(config_file) 30 | llm = ChatOpenAI(model="gpt-4o-mini") 31 | 32 | # Create agent with memory_enabled=True 33 | agent = MCPAgent( 34 | llm=llm, 35 | client=client, 36 | max_steps=15, 37 | memory_enabled=True, # Enable built-in conversation memory 38 | ) 39 | 40 | print("\n===== Interactive MCP Chat =====") 41 | print("Type 'exit' or 'quit' to end the conversation") 42 | print("Type 'clear' to clear conversation history") 43 | print("==================================\n") 44 | 45 | try: 46 | # Main chat loop 47 | while True: 48 | # Get user input 49 | user_input = input("\nYou: ") 50 | 51 | # Check for exit command 52 | if user_input.lower() in ["exit", "quit"]: 53 | print("Ending conversation...") 54 | break 55 | 56 | # Check for clear history command 57 | if user_input.lower() == "clear": 58 | agent.clear_conversation_history() 59 | print("Conversation history cleared.") 60 | continue 61 | 62 | # Get response from agent 63 | print("\nAssistant: ", end="", flush=True) 64 | 65 | try: 66 | # Run the agent with the user input (memory handling is automatic) 67 | response = await agent.run(user_input) 68 | print(response) 69 | 70 | except Exception as e: 71 | print(f"\nError: {e}") 72 | 73 | finally: 74 | # Clean up 75 | if client and client.sessions: 76 | await client.close_all_sessions() 77 | 78 | 79 | if __name__ == "__main__": 80 | asyncio.run(run_memory_chat()) 81 | -------------------------------------------------------------------------------- /examples/filesystem_use.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic usage example for mcp_use. 3 | 4 | This example demonstrates how to use the mcp_use library with MCPClient 5 | to connect any LLM to MCP tools through a unified interface. 6 | 7 | Special Thanks to https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem 8 | for the server. 9 | """ 10 | 11 | import asyncio 12 | 13 | from dotenv import load_dotenv 14 | from langchain_openai import ChatOpenAI 15 | 16 | from mcp_use import MCPAgent, MCPClient 17 | 18 | config = { 19 | "mcpServers": { 20 | "filesystem": { 21 | "command": "npx", 22 | "args": [ 23 | "-y", 24 | "@modelcontextprotocol/server-filesystem", 25 | "THE_PATH_TO_YOUR_DIRECTORY", 26 | ], 27 | } 28 | } 29 | } 30 | 31 | 32 | async def main(): 33 | """Run the example using a configuration file.""" 34 | # Load environment variables 35 | load_dotenv() 36 | 37 | # Create MCPClient from config file 38 | client = MCPClient.from_dict(config) 39 | # Create LLM 40 | llm = ChatOpenAI(model="gpt-4o") 41 | # llm = init_chat_model(model="llama-3.1-8b-instant", model_provider="groq") 42 | # llm = ChatAnthropic(model="claude-3-") 43 | # llm = ChatGroq(model="llama3-8b-8192") 44 | 45 | # Create agent with the client 46 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 47 | 48 | # Run the query 49 | result = await agent.run( 50 | "Hello can you give me a list of files and directories in the current directory", 51 | max_steps=30, 52 | ) 53 | print(f"\nResult: {result}") 54 | 55 | 56 | if __name__ == "__main__": 57 | # Run the appropriate example 58 | asyncio.run(main()) 59 | -------------------------------------------------------------------------------- /examples/http_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP Example for mcp_use. 3 | 4 | This example demonstrates how to use the mcp_use library with MCPClient 5 | to connect to an MCP server running on a specific HTTP port. 6 | 7 | Before running this example, you need to start the Playwright MCP server 8 | in another terminal with: 9 | 10 | npx @playwright/mcp@latest --port 8931 11 | 12 | This will start the server on port 8931. Resulting in the config you find below. 13 | Of course you can run this with any server you want at any URL. 14 | 15 | Special thanks to https://github.com/microsoft/playwright-mcp for the server. 16 | 17 | """ 18 | 19 | import asyncio 20 | 21 | from dotenv import load_dotenv 22 | from langchain_openai import ChatOpenAI 23 | 24 | from mcp_use import MCPAgent, MCPClient 25 | 26 | 27 | async def main(): 28 | """Run the example using a configuration file.""" 29 | # Load environment variables 30 | load_dotenv() 31 | 32 | config = {"mcpServers": {"http": {"url": "http://localhost:8931/sse"}}} 33 | 34 | # Create MCPClient from config file 35 | client = MCPClient.from_dict(config) 36 | 37 | # Create LLM 38 | llm = ChatOpenAI(model="gpt-4o") 39 | 40 | # Create agent with the client 41 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 42 | 43 | # Run the query 44 | result = await agent.run( 45 | "Find the best restaurant in San Francisco USING GOOGLE SEARCH", 46 | max_steps=30, 47 | ) 48 | print(f"\nResult: {result}") 49 | 50 | 51 | if __name__ == "__main__": 52 | # Run the appropriate example 53 | asyncio.run(main()) 54 | -------------------------------------------------------------------------------- /examples/multi_server_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example demonstrating how to use MCPClient with multiple servers. 3 | 4 | This example shows how to: 5 | 1. Configure multiple MCP servers 6 | 2. Create and manage sessions for each server 7 | 3. Use tools from different servers in a single agent 8 | """ 9 | 10 | import asyncio 11 | 12 | from dotenv import load_dotenv 13 | from langchain_anthropic import ChatAnthropic 14 | 15 | from mcp_use import MCPAgent, MCPClient 16 | 17 | 18 | async def run_multi_server_example(): 19 | """Run an example using multiple MCP servers.""" 20 | # Load environment variables 21 | load_dotenv() 22 | 23 | # Create a configuration with multiple servers 24 | config = { 25 | "mcpServers": { 26 | "airbnb": { 27 | "command": "npx", 28 | "args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"], 29 | }, 30 | "playwright": { 31 | "command": "npx", 32 | "args": ["@playwright/mcp@latest"], 33 | "env": {"DISPLAY": ":1"}, 34 | }, 35 | "filesystem": { 36 | "command": "npx", 37 | "args": [ 38 | "-y", 39 | "@modelcontextprotocol/server-filesystem", 40 | "YOUR_DIRECTORY_HERE", 41 | ], 42 | }, 43 | } 44 | } 45 | 46 | # Create MCPClient with the multi-server configuration 47 | client = MCPClient.from_dict(config) 48 | 49 | # Create LLM 50 | llm = ChatAnthropic(model="claude-3-5-sonnet-20240620") 51 | 52 | # Create agent with the client 53 | agent = MCPAgent(llm=llm, client=client, max_steps=30) 54 | 55 | # Example 1: Using tools from different servers in a single query 56 | result = await agent.run( 57 | "Search for a nice place to stay in Barcelona on Airbnb, " 58 | "then use Google to find nearby restaurants and attractions." 59 | "Write the result in the current directory in restarant.txt", 60 | max_steps=30, 61 | ) 62 | print(result) 63 | 64 | 65 | if __name__ == "__main__": 66 | asyncio.run(run_multi_server_example()) 67 | -------------------------------------------------------------------------------- /mcp_use/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | mcp_use - An MCP library for LLMs. 3 | 4 | This library provides a unified interface for connecting different LLMs 5 | to MCP tools through existing LangChain adapters. 6 | """ 7 | 8 | from importlib.metadata import version 9 | 10 | from .agents.mcpagent import MCPAgent 11 | from .client import MCPClient 12 | from .config import load_config_file 13 | from .connectors import BaseConnector, HttpConnector, StdioConnector, WebSocketConnector 14 | from .logging import MCP_USE_DEBUG, Logger, logger 15 | from .session import MCPSession 16 | 17 | __version__ = version("mcp-use") 18 | 19 | __all__ = [ 20 | "MCPAgent", 21 | "MCPClient", 22 | "MCPSession", 23 | "BaseConnector", 24 | "StdioConnector", 25 | "WebSocketConnector", 26 | "HttpConnector", 27 | "create_session_from_config", 28 | "load_config_file", 29 | "logger", 30 | "MCP_USE_DEBUG", 31 | "Logger", 32 | "set_debug", 33 | ] 34 | 35 | 36 | # Helper function to set debug mode 37 | def set_debug(debug=2): 38 | """Set the debug mode for mcp_use. 39 | 40 | Args: 41 | debug: Whether to enable debug mode (default: True) 42 | """ 43 | Logger.set_debug(debug) 44 | -------------------------------------------------------------------------------- /mcp_use/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adapters for converting MCP tools to different frameworks. 3 | 4 | This package provides adapters for converting MCP tools to different frameworks. 5 | """ 6 | 7 | from .base import BaseAdapter 8 | from .langchain_adapter import LangChainAdapter 9 | 10 | __all__ = ["BaseAdapter", "LangChainAdapter"] 11 | -------------------------------------------------------------------------------- /mcp_use/adapters/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base adapter interface for MCP tools. 3 | 4 | This module provides the abstract base class that all MCP tool adapters should inherit from. 5 | """ 6 | 7 | from abc import ABC, abstractmethod 8 | from typing import Any, TypeVar 9 | 10 | from ..client import MCPClient 11 | from ..connectors.base import BaseConnector 12 | from ..logging import logger 13 | 14 | # Generic type for the tools created by the adapter 15 | T = TypeVar("T") 16 | 17 | 18 | class BaseAdapter(ABC): 19 | """Abstract base class for converting MCP tools to other framework formats. 20 | 21 | This class defines the common interface that all adapter implementations 22 | should follow to ensure consistency across different frameworks. 23 | """ 24 | 25 | def __init__(self, disallowed_tools: list[str] | None = None) -> None: 26 | """Initialize a new adapter. 27 | 28 | Args: 29 | disallowed_tools: list of tool names that should not be available. 30 | """ 31 | self.disallowed_tools = disallowed_tools or [] 32 | self._connector_tool_map: dict[BaseConnector, list[T]] = {} 33 | 34 | @classmethod 35 | async def create_tools( 36 | cls, client: "MCPClient", disallowed_tools: list[str] | None = None 37 | ) -> list[T]: 38 | """Create tools from an MCPClient instance. 39 | 40 | This is the recommended way to create tools from an MCPClient, as it handles 41 | session creation and connector extraction automatically. 42 | 43 | Args: 44 | client: The MCPClient to extract tools from. 45 | disallowed_tools: Optional list of tool names to exclude. 46 | 47 | Returns: 48 | A list of tools in the target framework's format. 49 | 50 | Example: 51 | ```python 52 | from mcp_use.client import MCPClient 53 | from mcp_use.adapters import YourAdapter 54 | 55 | client = MCPClient.from_config_file("config.json") 56 | tools = await YourAdapter.create_tools(client) 57 | ``` 58 | """ 59 | # Create the adapter 60 | adapter = cls(disallowed_tools=disallowed_tools) 61 | 62 | # Ensure we have active sessions 63 | if not client.active_sessions: 64 | logger.info("No active sessions found, creating new ones...") 65 | await client.create_all_sessions() 66 | 67 | # Get all active sessions 68 | sessions = client.get_all_active_sessions() 69 | 70 | # Extract connectors from sessions 71 | connectors = [session.connector for session in sessions.values()] 72 | 73 | # Create tools from connectors 74 | return await adapter._create_tools_from_connectors(connectors) 75 | 76 | async def load_tools_for_connector(self, connector: BaseConnector) -> list[T]: 77 | """Dynamically load tools for a specific connector. 78 | 79 | Args: 80 | connector: The connector to load tools for. 81 | 82 | Returns: 83 | The list of tools that were loaded in the target framework's format. 84 | """ 85 | # Check if we already have tools for this connector 86 | if connector in self._connector_tool_map: 87 | logger.debug( 88 | f"Returning {len(self._connector_tool_map[connector])} existing tools for connector" 89 | ) 90 | return self._connector_tool_map[connector] 91 | 92 | # Create tools for this connector 93 | connector_tools = [] 94 | 95 | # Make sure the connector is initialized and has tools 96 | success = await self._ensure_connector_initialized(connector) 97 | if not success: 98 | return [] 99 | 100 | # Now create tools for each MCP tool 101 | for tool in connector.tools: 102 | # Convert the tool and add it to the list if conversion was successful 103 | converted_tool = self._convert_tool(tool, connector) 104 | if converted_tool: 105 | connector_tools.append(converted_tool) 106 | 107 | # Store the tools for this connector 108 | self._connector_tool_map[connector] = connector_tools 109 | 110 | # Log available tools for debugging 111 | logger.debug( 112 | f"Loaded {len(connector_tools)} new tools for connector: " 113 | f"{[getattr(tool, 'name', str(tool)) for tool in connector_tools]}" 114 | ) 115 | 116 | return connector_tools 117 | 118 | @abstractmethod 119 | def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> T: 120 | """Convert an MCP tool to the target framework's tool format. 121 | 122 | Args: 123 | mcp_tool: The MCP tool to convert. 124 | connector: The connector that provides this tool. 125 | 126 | Returns: 127 | A tool in the target framework's format. 128 | """ 129 | pass 130 | 131 | async def _create_tools_from_connectors(self, connectors: list[BaseConnector]) -> list[T]: 132 | """Create tools from MCP tools in all provided connectors. 133 | 134 | Args: 135 | connectors: list of MCP connectors to create tools from. 136 | 137 | Returns: 138 | A list of tools in the target framework's format. 139 | """ 140 | tools = [] 141 | for connector in connectors: 142 | # Create tools for this connector 143 | connector_tools = await self.load_tools_for_connector(connector) 144 | tools.extend(connector_tools) 145 | 146 | # Log available tools for debugging 147 | logger.debug(f"Available tools: {len(tools)}") 148 | return tools 149 | 150 | def _check_connector_initialized(self, connector: BaseConnector) -> bool: 151 | """Check if a connector is initialized and has tools. 152 | 153 | Args: 154 | connector: The connector to check. 155 | 156 | Returns: 157 | True if the connector is initialized and has tools, False otherwise. 158 | """ 159 | return hasattr(connector, "tools") and connector.tools 160 | 161 | async def _ensure_connector_initialized(self, connector: BaseConnector) -> bool: 162 | """Ensure a connector is initialized. 163 | 164 | Args: 165 | connector: The connector to initialize. 166 | 167 | Returns: 168 | True if initialization succeeded, False otherwise. 169 | """ 170 | if not self._check_connector_initialized(connector): 171 | logger.debug("Connector doesn't have tools, initializing it") 172 | try: 173 | await connector.initialize() 174 | return True 175 | except Exception as e: 176 | logger.error(f"Error initializing connector: {e}") 177 | return False 178 | return True 179 | -------------------------------------------------------------------------------- /mcp_use/adapters/langchain_adapter.py: -------------------------------------------------------------------------------- 1 | """ 2 | LangChain adapter for MCP tools. 3 | 4 | This module provides utilities to convert MCP tools to LangChain tools. 5 | """ 6 | 7 | from typing import Any, NoReturn 8 | 9 | from jsonschema_pydantic import jsonschema_to_pydantic 10 | from langchain_core.tools import BaseTool, ToolException 11 | from mcp.types import CallToolResult, EmbeddedResource, ImageContent, TextContent 12 | from pydantic import BaseModel 13 | 14 | from ..connectors.base import BaseConnector 15 | from ..logging import logger 16 | from .base import BaseAdapter 17 | 18 | 19 | class LangChainAdapter(BaseAdapter): 20 | """Adapter for converting MCP tools to LangChain tools.""" 21 | 22 | def __init__(self, disallowed_tools: list[str] | None = None) -> None: 23 | """Initialize a new LangChain adapter. 24 | 25 | Args: 26 | disallowed_tools: list of tool names that should not be available. 27 | """ 28 | super().__init__(disallowed_tools) 29 | self._connector_tool_map: dict[BaseConnector, list[BaseTool]] = {} 30 | 31 | def fix_schema(self, schema: dict) -> dict: 32 | """Convert JSON Schema 'type': ['string', 'null'] to 'anyOf' format. 33 | 34 | Args: 35 | schema: The JSON schema to fix. 36 | 37 | Returns: 38 | The fixed JSON schema. 39 | """ 40 | if isinstance(schema, dict): 41 | if "type" in schema and isinstance(schema["type"], list): 42 | schema["anyOf"] = [{"type": t} for t in schema["type"]] 43 | del schema["type"] # Remove 'type' and standardize to 'anyOf' 44 | for key, value in schema.items(): 45 | schema[key] = self.fix_schema(value) # Apply recursively 46 | return schema 47 | 48 | def _parse_mcp_tool_result(self, tool_result: CallToolResult) -> str: 49 | """Parse the content of a CallToolResult into a string. 50 | 51 | Args: 52 | tool_result: The result object from calling an MCP tool. 53 | 54 | Returns: 55 | A string representation of the tool result content. 56 | 57 | Raises: 58 | ToolException: If the tool execution failed, returned no content, 59 | or contained unexpected content types. 60 | """ 61 | if tool_result.isError: 62 | raise ToolException(f"Tool execution failed: {tool_result.content}") 63 | 64 | if not tool_result.content: 65 | raise ToolException("Tool execution returned no content") 66 | 67 | decoded_result = "" 68 | for item in tool_result.content: 69 | match item.type: 70 | case "text": 71 | item: TextContent 72 | decoded_result += item.text 73 | case "image": 74 | item: ImageContent 75 | decoded_result += item.data # Assuming data is string-like or base64 76 | case "resource": 77 | resource: EmbeddedResource = item.resource 78 | if hasattr(resource, "text"): 79 | decoded_result += resource.text 80 | elif hasattr(resource, "blob"): 81 | # Assuming blob needs decoding or specific handling; adjust as needed 82 | decoded_result += ( 83 | resource.blob.decode() 84 | if isinstance(resource.blob, bytes) 85 | else str(resource.blob) 86 | ) 87 | else: 88 | raise ToolException(f"Unexpected resource type: {resource.type}") 89 | case _: 90 | raise ToolException(f"Unexpected content type: {item.type}") 91 | 92 | return decoded_result 93 | 94 | def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> BaseTool: 95 | """Convert an MCP tool to LangChain's tool format. 96 | 97 | Args: 98 | mcp_tool: The MCP tool to convert. 99 | connector: The connector that provides this tool. 100 | 101 | Returns: 102 | A LangChain BaseTool. 103 | """ 104 | # Skip disallowed tools 105 | if mcp_tool.name in self.disallowed_tools: 106 | return None 107 | 108 | # This is a dynamic class creation, we need to work with the self reference 109 | adapter_self = self 110 | 111 | class McpToLangChainAdapter(BaseTool): 112 | name: str = mcp_tool.name or "NO NAME" 113 | description: str = mcp_tool.description or "" 114 | # Convert JSON schema to Pydantic model for argument validation 115 | args_schema: type[BaseModel] = jsonschema_to_pydantic( 116 | adapter_self.fix_schema(mcp_tool.inputSchema) # Apply schema conversion 117 | ) 118 | tool_connector: BaseConnector = connector # Renamed variable to avoid name conflict 119 | handle_tool_error: bool = True 120 | 121 | def __repr__(self) -> str: 122 | return f"MCP tool: {self.name}: {self.description}" 123 | 124 | def _run(self, **kwargs: Any) -> NoReturn: 125 | """Synchronous run method that always raises an error. 126 | 127 | Raises: 128 | NotImplementedError: Always raises this error because MCP tools 129 | only support async operations. 130 | """ 131 | raise NotImplementedError("MCP tools only support async operations") 132 | 133 | async def _arun(self, **kwargs: Any) -> Any: 134 | """Asynchronously execute the tool with given arguments. 135 | 136 | Args: 137 | kwargs: The arguments to pass to the tool. 138 | 139 | Returns: 140 | The result of the tool execution. 141 | 142 | Raises: 143 | ToolException: If tool execution fails. 144 | """ 145 | logger.debug(f'MCP tool: "{self.name}" received input: {kwargs}') 146 | 147 | try: 148 | tool_result: CallToolResult = await self.tool_connector.call_tool( 149 | self.name, kwargs 150 | ) 151 | try: 152 | # Use the helper function to parse the result 153 | return adapter_self._parse_mcp_tool_result(tool_result) 154 | except Exception as e: 155 | # Log the exception for debugging 156 | logger.error(f"Error parsing tool result: {e}") 157 | return f"Error parsing result: {e!s}; Raw content: {tool_result.content!r}" 158 | 159 | except Exception as e: 160 | if self.handle_tool_error: 161 | return f"Error executing MCP tool: {str(e)}" 162 | raise 163 | 164 | return McpToLangChainAdapter() 165 | -------------------------------------------------------------------------------- /mcp_use/agents/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Agent implementations for using MCP tools. 3 | 4 | This module provides ready-to-use agent implementations 5 | that are pre-configured for using MCP tools. 6 | """ 7 | 8 | from .base import BaseAgent 9 | from .mcpagent import MCPAgent 10 | 11 | __all__ = [ 12 | "BaseAgent", 13 | "MCPAgent", 14 | ] 15 | -------------------------------------------------------------------------------- /mcp_use/agents/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base agent interface for MCP tools. 3 | 4 | This module provides a base class for agents that use MCP tools. 5 | """ 6 | 7 | from abc import ABC, abstractmethod 8 | from typing import Any 9 | 10 | from ..session import MCPSession 11 | 12 | 13 | class BaseAgent(ABC): 14 | """Base class for agents that use MCP tools. 15 | 16 | This abstract class defines the interface for agents that use MCP tools. 17 | Agents are responsible for integrating LLMs with MCP tools. 18 | """ 19 | 20 | def __init__(self, session: MCPSession): 21 | """Initialize a new agent. 22 | 23 | Args: 24 | session: The MCP session to use for tool calls. 25 | """ 26 | self.session = session 27 | 28 | @abstractmethod 29 | async def initialize(self) -> None: 30 | """Initialize the agent. 31 | 32 | This method should prepare the agent for use, including initializing 33 | the MCP session and setting up any necessary components. 34 | """ 35 | pass 36 | 37 | @abstractmethod 38 | async def run(self, query: str, max_steps: int = 10) -> dict[str, Any]: 39 | """Run the agent with a query. 40 | 41 | Args: 42 | query: The query to run. 43 | max_steps: The maximum number of steps to run. 44 | 45 | Returns: 46 | The final result from the agent. 47 | """ 48 | pass 49 | 50 | @abstractmethod 51 | async def step( 52 | self, query: str, previous_steps: list[dict[str, Any]] | None = None 53 | ) -> dict[str, Any]: 54 | """Perform a single step of the agent. 55 | 56 | Args: 57 | query: The query to run. 58 | previous_steps: Optional list of previous steps. 59 | 60 | Returns: 61 | The result of the step. 62 | """ 63 | pass 64 | -------------------------------------------------------------------------------- /mcp_use/agents/prompts/system_prompt_builder.py: -------------------------------------------------------------------------------- 1 | from langchain.schema import SystemMessage 2 | from langchain_core.tools import BaseTool 3 | 4 | 5 | def generate_tool_descriptions( 6 | tools: list[BaseTool], disallowed_tools: list[str] | None = None 7 | ) -> list[str]: 8 | """ 9 | Generates a list of formatted tool descriptions, excluding disallowed tools. 10 | 11 | Args: 12 | tools: The list of available BaseTool objects. 13 | disallowed_tools: A list of tool names to exclude. 14 | 15 | Returns: 16 | A list of strings, each describing a tool in the format "- tool_name: description". 17 | """ 18 | disallowed_set = set(disallowed_tools or []) 19 | tool_descriptions_list = [] 20 | for tool in tools: 21 | if tool.name in disallowed_set: 22 | continue 23 | # Escape curly braces for formatting 24 | escaped_desc = tool.description.replace("{", "{{").replace("}", "}}") 25 | description = f"- {tool.name}: {escaped_desc}" 26 | tool_descriptions_list.append(description) 27 | return tool_descriptions_list 28 | 29 | 30 | def build_system_prompt_content( 31 | template: str, tool_description_lines: list[str], additional_instructions: str | None = None 32 | ) -> str: 33 | """ 34 | Builds the final system prompt string using a template, tool descriptions, 35 | and optional additional instructions. 36 | 37 | Args: 38 | template: The system prompt template string (must contain '{tool_descriptions}'). 39 | tool_description_lines: A list of formatted tool description strings. 40 | additional_instructions: Optional extra instructions to append. 41 | 42 | Returns: 43 | The fully formatted system prompt content string. 44 | """ 45 | tool_descriptions_block = "\n".join(tool_description_lines) 46 | # Add a check for missing placeholder to prevent errors 47 | if "{tool_descriptions}" not in template: 48 | # Handle this case: maybe append descriptions at the end or raise an error 49 | # For now, let's append if placeholder is missing 50 | print("Warning: '{tool_descriptions}' placeholder not found in template.") 51 | system_prompt_content = template + "\n\nAvailable tools:\n" + tool_descriptions_block 52 | else: 53 | system_prompt_content = template.format(tool_descriptions=tool_descriptions_block) 54 | 55 | if additional_instructions: 56 | system_prompt_content += f"\n\n{additional_instructions}" 57 | 58 | return system_prompt_content 59 | 60 | 61 | def create_system_message( 62 | tools: list[BaseTool], 63 | system_prompt_template: str, 64 | server_manager_template: str, 65 | use_server_manager: bool, 66 | disallowed_tools: list[str] | None = None, 67 | user_provided_prompt: str | None = None, 68 | additional_instructions: str | None = None, 69 | ) -> SystemMessage: 70 | """ 71 | Creates the final SystemMessage object for the agent. 72 | 73 | Handles selecting the correct template, generating tool descriptions, 74 | and incorporating user overrides and additional instructions. 75 | 76 | Args: 77 | tools: List of available tools. 78 | system_prompt_template: The default system prompt template. 79 | server_manager_template: The template to use when server manager is active. 80 | use_server_manager: Flag indicating if server manager mode is enabled. 81 | disallowed_tools: List of tool names to exclude. 82 | user_provided_prompt: A complete system prompt provided by the user, overriding templates. 83 | additional_instructions: Extra instructions to append to the template-based prompt. 84 | 85 | Returns: 86 | A SystemMessage object containing the final prompt content. 87 | """ 88 | # If a complete user prompt is given, use it directly 89 | if user_provided_prompt: 90 | return SystemMessage(content=user_provided_prompt) 91 | 92 | # Select the appropriate template 93 | template_to_use = server_manager_template if use_server_manager else system_prompt_template 94 | 95 | # Generate tool descriptions 96 | tool_description_lines = generate_tool_descriptions(tools, disallowed_tools) 97 | 98 | # Build the final prompt content 99 | final_prompt_content = build_system_prompt_content( 100 | template=template_to_use, 101 | tool_description_lines=tool_description_lines, 102 | additional_instructions=additional_instructions, 103 | ) 104 | 105 | return SystemMessage(content=final_prompt_content) 106 | -------------------------------------------------------------------------------- /mcp_use/agents/prompts/templates.py: -------------------------------------------------------------------------------- 1 | # mcp_use/agents/prompts/templates.py 2 | 3 | DEFAULT_SYSTEM_PROMPT_TEMPLATE = """You are a helpful AI assistant. 4 | You have access to the following tools: 5 | 6 | {tool_descriptions} 7 | 8 | Use the following format: 9 | 10 | Question: the input question you must answer 11 | Thought: you should always think about what to do 12 | Action: the action to take, should be one of the available tools 13 | Action Input: the input to the action 14 | Observation: the result of the action 15 | ... (this Thought/Action/Action Input/Observation can repeat N times) 16 | Thought: I now know the final answer 17 | Final Answer: the final answer to the original input question""" 18 | 19 | 20 | SERVER_MANAGER_SYSTEM_PROMPT_TEMPLATE = """You are a helpful assistant designed to interact with MCP 21 | (Model Context Protocol) servers. You can manage connections to different servers and use the tools 22 | provided by the currently active server. 23 | 24 | Important: The available tools change depending on which server is active. 25 | If a request requires tools not listed below (e.g., file operations, web browsing, 26 | image manipulation), you MUST first connect to the appropriate server using 27 | 'connect_to_mcp_server'. 28 | Use 'list_mcp_servers' to find the relevant server if you are unsure. 29 | Only after successfully connecting and seeing the new tools listed in 30 | the response should you attempt to use those server-specific tools. 31 | Before attempting a task that requires specific tools, you should 32 | ensure you are connected to the correct server and aware of its 33 | available tools. If unsure, use 'list_mcp_servers' to see options 34 | or 'get_active_mcp_server' to check the current connection. 35 | 36 | When you connect to a server using 'connect_to_mcp_server', 37 | you will be informed about the new tools that become available. 38 | You can then use these server-specific tools in subsequent steps. 39 | 40 | Here are the tools *currently* available to you (this list includes server management tools and will 41 | change when you connect to a server): 42 | {tool_descriptions} 43 | """ 44 | -------------------------------------------------------------------------------- /mcp_use/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Client for managing MCP servers and sessions. 3 | 4 | This module provides a high-level client that manages MCP servers, connectors, 5 | and sessions from configuration. 6 | """ 7 | 8 | import json 9 | from typing import Any 10 | 11 | from .config import create_connector_from_config, load_config_file 12 | from .logging import logger 13 | from .session import MCPSession 14 | 15 | 16 | class MCPClient: 17 | """Client for managing MCP servers and sessions. 18 | 19 | This class provides a unified interface for working with MCP servers, 20 | handling configuration, connector creation, and session management. 21 | """ 22 | 23 | def __init__( 24 | self, 25 | config: str | dict[str, Any] | None = None, 26 | ) -> None: 27 | """Initialize a new MCP client. 28 | 29 | Args: 30 | config: Either a dict containing configuration or a path to a JSON config file. 31 | If None, an empty configuration is used. 32 | """ 33 | self.config: dict[str, Any] = {} 34 | self.sessions: dict[str, MCPSession] = {} 35 | self.active_sessions: list[str] = [] 36 | 37 | # Load configuration if provided 38 | if config is not None: 39 | if isinstance(config, str): 40 | self.config = load_config_file(config) 41 | else: 42 | self.config = config 43 | 44 | @classmethod 45 | def from_dict(cls, config: dict[str, Any]) -> "MCPClient": 46 | """Create a MCPClient from a dictionary. 47 | 48 | Args: 49 | config: The configuration dictionary. 50 | """ 51 | return cls(config=config) 52 | 53 | @classmethod 54 | def from_config_file(cls, filepath: str) -> "MCPClient": 55 | """Create a MCPClient from a configuration file. 56 | 57 | Args: 58 | filepath: The path to the configuration file. 59 | """ 60 | return cls(config=load_config_file(filepath)) 61 | 62 | def add_server( 63 | self, 64 | name: str, 65 | server_config: dict[str, Any], 66 | ) -> None: 67 | """Add a server configuration. 68 | 69 | Args: 70 | name: The name to identify this server. 71 | server_config: The server configuration. 72 | """ 73 | if "mcpServers" not in self.config: 74 | self.config["mcpServers"] = {} 75 | 76 | self.config["mcpServers"][name] = server_config 77 | 78 | def remove_server(self, name: str) -> None: 79 | """Remove a server configuration. 80 | 81 | Args: 82 | name: The name of the server to remove. 83 | """ 84 | if "mcpServers" in self.config and name in self.config["mcpServers"]: 85 | del self.config["mcpServers"][name] 86 | 87 | # If we removed an active session, remove it from active_sessions 88 | if name in self.active_sessions: 89 | self.active_sessions.remove(name) 90 | 91 | def get_server_names(self) -> list[str]: 92 | """Get the list of configured server names. 93 | 94 | Returns: 95 | List of server names. 96 | """ 97 | return list(self.config.get("mcpServers", {}).keys()) 98 | 99 | def save_config(self, filepath: str) -> None: 100 | """Save the current configuration to a file. 101 | 102 | Args: 103 | filepath: The path to save the configuration to. 104 | """ 105 | with open(filepath, "w") as f: 106 | json.dump(self.config, f, indent=2) 107 | 108 | async def create_session(self, server_name: str, auto_initialize: bool = True) -> MCPSession: 109 | """Create a session for the specified server. 110 | 111 | Args: 112 | server_name: The name of the server to create a session for. 113 | 114 | Returns: 115 | The created MCPSession. 116 | 117 | Raises: 118 | ValueError: If no servers are configured or the specified server doesn't exist. 119 | """ 120 | # Get server config 121 | servers = self.config.get("mcpServers", {}) 122 | if not servers: 123 | raise ValueError("No MCP servers defined in config") 124 | 125 | if server_name not in servers: 126 | raise ValueError(f"Server '{server_name}' not found in config") 127 | 128 | server_config = servers[server_name] 129 | connector = create_connector_from_config(server_config) 130 | 131 | # Create the session 132 | session = MCPSession(connector) 133 | if auto_initialize: 134 | await session.initialize() 135 | self.sessions[server_name] = session 136 | 137 | # Add to active sessions 138 | if server_name not in self.active_sessions: 139 | self.active_sessions.append(server_name) 140 | 141 | return session 142 | 143 | async def create_all_sessions( 144 | self, 145 | auto_initialize: bool = True, 146 | ) -> dict[str, MCPSession]: 147 | """Create a session for the specified server. 148 | 149 | Args: 150 | auto_initialize: Whether to automatically initialize the session. 151 | 152 | Returns: 153 | The created MCPSession. If server_name is None, returns the first created session. 154 | 155 | Raises: 156 | ValueError: If no servers are configured or the specified server doesn't exist. 157 | """ 158 | # Get server config 159 | servers = self.config.get("mcpServers", {}) 160 | if not servers: 161 | raise ValueError("No MCP servers defined in config") 162 | 163 | # Create sessions for all servers 164 | for name in servers: 165 | session = await self.create_session(name, auto_initialize) 166 | if auto_initialize: 167 | await session.initialize() 168 | 169 | return self.sessions 170 | 171 | def get_session(self, server_name: str) -> MCPSession: 172 | """Get an existing session. 173 | 174 | Args: 175 | server_name: The name of the server to get the session for. 176 | If None, uses the first active session. 177 | 178 | Returns: 179 | The MCPSession for the specified server. 180 | 181 | Raises: 182 | ValueError: If no active sessions exist or the specified session doesn't exist. 183 | """ 184 | if server_name not in self.sessions: 185 | raise ValueError(f"No session exists for server '{server_name}'") 186 | 187 | return self.sessions[server_name] 188 | 189 | def get_all_active_sessions(self) -> dict[str, MCPSession]: 190 | """Get all active sessions. 191 | 192 | Returns: 193 | Dictionary mapping server names to their MCPSession instances. 194 | """ 195 | return {name: self.sessions[name] for name in self.active_sessions if name in self.sessions} 196 | 197 | async def close_session(self, server_name: str) -> None: 198 | """Close a session. 199 | 200 | Args: 201 | server_name: The name of the server to close the session for. 202 | If None, uses the first active session. 203 | 204 | Raises: 205 | ValueError: If no active sessions exist or the specified session doesn't exist. 206 | """ 207 | # Check if the session exists 208 | if server_name not in self.sessions: 209 | logger.warning(f"No session exists for server '{server_name}', nothing to close") 210 | return 211 | 212 | # Get the session 213 | session = self.sessions[server_name] 214 | 215 | try: 216 | # Disconnect from the session 217 | logger.debug(f"Closing session for server '{server_name}'") 218 | await session.disconnect() 219 | except Exception as e: 220 | logger.error(f"Error closing session for server '{server_name}': {e}") 221 | finally: 222 | # Remove the session regardless of whether disconnect succeeded 223 | del self.sessions[server_name] 224 | 225 | # Remove from active_sessions 226 | if server_name in self.active_sessions: 227 | self.active_sessions.remove(server_name) 228 | 229 | async def close_all_sessions(self) -> None: 230 | """Close all active sessions. 231 | 232 | This method ensures all sessions are closed even if some fail. 233 | """ 234 | # Get a list of all session names first to avoid modification during iteration 235 | server_names = list(self.sessions.keys()) 236 | errors = [] 237 | 238 | for server_name in server_names: 239 | try: 240 | logger.debug(f"Closing session for server '{server_name}'") 241 | await self.close_session(server_name) 242 | except Exception as e: 243 | error_msg = f"Failed to close session for server '{server_name}': {e}" 244 | logger.error(error_msg) 245 | errors.append(error_msg) 246 | 247 | # Log summary if there were errors 248 | if errors: 249 | logger.error(f"Encountered {len(errors)} errors while closing sessions") 250 | else: 251 | logger.debug("All sessions closed successfully") 252 | -------------------------------------------------------------------------------- /mcp_use/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration loader for MCP session. 3 | 4 | This module provides functionality to load MCP configuration from JSON files. 5 | """ 6 | 7 | import json 8 | from typing import Any 9 | 10 | from .connectors import BaseConnector, HttpConnector, StdioConnector, WebSocketConnector 11 | 12 | 13 | def load_config_file(filepath: str) -> dict[str, Any]: 14 | """Load a configuration file. 15 | 16 | Args: 17 | filepath: Path to the configuration file 18 | 19 | Returns: 20 | The parsed configuration 21 | """ 22 | with open(filepath) as f: 23 | return json.load(f) 24 | 25 | 26 | def create_connector_from_config(server_config: dict[str, Any]) -> BaseConnector: 27 | """Create a connector based on server configuration. 28 | 29 | Args: 30 | server_config: The server configuration section 31 | 32 | Returns: 33 | A configured connector instance 34 | """ 35 | # Stdio connector (command-based) 36 | if "command" in server_config and "args" in server_config: 37 | return StdioConnector( 38 | command=server_config["command"], 39 | args=server_config["args"], 40 | env=server_config.get("env", None), 41 | ) 42 | 43 | # HTTP connector 44 | elif "url" in server_config: 45 | return HttpConnector( 46 | base_url=server_config["url"], 47 | headers=server_config.get("headers", None), 48 | auth_token=server_config.get("auth_token", None), 49 | ) 50 | 51 | # WebSocket connector 52 | elif "ws_url" in server_config: 53 | return WebSocketConnector( 54 | url=server_config["ws_url"], 55 | headers=server_config.get("headers", None), 56 | auth_token=server_config.get("auth_token", None), 57 | ) 58 | 59 | raise ValueError("Cannot determine connector type from config") 60 | -------------------------------------------------------------------------------- /mcp_use/connectors/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connectors for various MCP transports. 3 | 4 | This module provides interfaces for connecting to MCP implementations 5 | through different transport mechanisms. 6 | """ 7 | 8 | from .base import BaseConnector 9 | from .http import HttpConnector 10 | from .stdio import StdioConnector 11 | from .websocket import WebSocketConnector 12 | 13 | __all__ = ["BaseConnector", "StdioConnector", "WebSocketConnector", "HttpConnector"] 14 | -------------------------------------------------------------------------------- /mcp_use/connectors/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base connector for MCP implementations. 3 | 4 | This module provides the base connector interface that all MCP connectors 5 | must implement. 6 | """ 7 | 8 | from abc import ABC, abstractmethod 9 | from typing import Any 10 | 11 | from mcp import ClientSession 12 | from mcp.types import CallToolResult, Tool 13 | 14 | from ..logging import logger 15 | from ..task_managers import ConnectionManager 16 | 17 | 18 | class BaseConnector(ABC): 19 | """Base class for MCP connectors. 20 | 21 | This class defines the interface that all MCP connectors must implement. 22 | """ 23 | 24 | def __init__(self): 25 | """Initialize base connector with common attributes.""" 26 | self.client: ClientSession | None = None 27 | self._connection_manager: ConnectionManager | None = None 28 | self._tools: list[Tool] | None = None 29 | self._connected = False 30 | 31 | @abstractmethod 32 | async def connect(self) -> None: 33 | """Establish a connection to the MCP implementation.""" 34 | pass 35 | 36 | async def disconnect(self) -> None: 37 | """Close the connection to the MCP implementation.""" 38 | if not self._connected: 39 | logger.debug("Not connected to MCP implementation") 40 | return 41 | 42 | logger.debug("Disconnecting from MCP implementation") 43 | await self._cleanup_resources() 44 | self._connected = False 45 | logger.debug("Disconnected from MCP implementation") 46 | 47 | async def _cleanup_resources(self) -> None: 48 | """Clean up all resources associated with this connector.""" 49 | errors = [] 50 | 51 | # First close the client session 52 | if self.client: 53 | try: 54 | logger.debug("Closing client session") 55 | await self.client.__aexit__(None, None, None) 56 | except Exception as e: 57 | error_msg = f"Error closing client session: {e}" 58 | logger.warning(error_msg) 59 | errors.append(error_msg) 60 | finally: 61 | self.client = None 62 | 63 | # Then stop the connection manager 64 | if self._connection_manager: 65 | try: 66 | logger.debug("Stopping connection manager") 67 | await self._connection_manager.stop() 68 | except Exception as e: 69 | error_msg = f"Error stopping connection manager: {e}" 70 | logger.warning(error_msg) 71 | errors.append(error_msg) 72 | finally: 73 | self._connection_manager = None 74 | 75 | # Reset tools 76 | self._tools = None 77 | 78 | if errors: 79 | logger.warning(f"Encountered {len(errors)} errors during resource cleanup") 80 | 81 | async def initialize(self) -> dict[str, Any]: 82 | """Initialize the MCP session and return session information.""" 83 | if not self.client: 84 | raise RuntimeError("MCP client is not connected") 85 | 86 | logger.debug("Initializing MCP session") 87 | 88 | # Initialize the session 89 | result = await self.client.initialize() 90 | 91 | # Get available tools 92 | tools_result = await self.client.list_tools() 93 | self._tools = tools_result.tools 94 | 95 | logger.debug(f"MCP session initialized with {len(self._tools)} tools") 96 | 97 | return result 98 | 99 | @property 100 | def tools(self) -> list[Tool]: 101 | """Get the list of available tools.""" 102 | if not self._tools: 103 | raise RuntimeError("MCP client is not initialized") 104 | return self._tools 105 | 106 | async def call_tool(self, name: str, arguments: dict[str, Any]) -> CallToolResult: 107 | """Call an MCP tool with the given arguments.""" 108 | if not self.client: 109 | raise RuntimeError("MCP client is not connected") 110 | 111 | logger.debug(f"Calling tool '{name}' with arguments: {arguments}") 112 | result = await self.client.call_tool(name, arguments) 113 | logger.debug(f"Tool '{name}' called with result: {result}") 114 | return result 115 | 116 | async def list_resources(self) -> list[dict[str, Any]]: 117 | """List all available resources from the MCP implementation.""" 118 | if not self.client: 119 | raise RuntimeError("MCP client is not connected") 120 | 121 | logger.debug("Listing resources") 122 | resources = await self.client.list_resources() 123 | return resources 124 | 125 | async def read_resource(self, uri: str) -> tuple[bytes, str]: 126 | """Read a resource by URI.""" 127 | if not self.client: 128 | raise RuntimeError("MCP client is not connected") 129 | 130 | logger.debug(f"Reading resource: {uri}") 131 | resource = await self.client.read_resource(uri) 132 | return resource.content, resource.mimeType 133 | 134 | async def request(self, method: str, params: dict[str, Any] | None = None) -> Any: 135 | """Send a raw request to the MCP implementation.""" 136 | if not self.client: 137 | raise RuntimeError("MCP client is not connected") 138 | 139 | logger.debug(f"Sending request: {method} with params: {params}") 140 | return await self.client.request({"method": method, "params": params or {}}) 141 | -------------------------------------------------------------------------------- /mcp_use/connectors/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP connector for MCP implementations. 3 | 4 | This module provides a connector for communicating with MCP implementations 5 | through HTTP APIs with SSE for transport. 6 | """ 7 | 8 | from mcp import ClientSession 9 | 10 | from ..logging import logger 11 | from ..task_managers import SseConnectionManager 12 | from .base import BaseConnector 13 | 14 | 15 | class HttpConnector(BaseConnector): 16 | """Connector for MCP implementations using HTTP transport with SSE. 17 | 18 | This connector uses HTTP/SSE to communicate with remote MCP implementations, 19 | using a connection manager to handle the proper lifecycle management. 20 | """ 21 | 22 | def __init__( 23 | self, 24 | base_url: str, 25 | auth_token: str | None = None, 26 | headers: dict[str, str] | None = None, 27 | timeout: float = 5, 28 | sse_read_timeout: float = 60 * 5, 29 | ): 30 | """Initialize a new HTTP connector. 31 | 32 | Args: 33 | base_url: The base URL of the MCP HTTP API. 34 | auth_token: Optional authentication token. 35 | headers: Optional additional headers. 36 | timeout: Timeout for HTTP operations in seconds. 37 | sse_read_timeout: Timeout for SSE read operations in seconds. 38 | """ 39 | super().__init__() 40 | self.base_url = base_url.rstrip("/") 41 | self.auth_token = auth_token 42 | self.headers = headers or {} 43 | if auth_token: 44 | self.headers["Authorization"] = f"Bearer {auth_token}" 45 | self.timeout = timeout 46 | self.sse_read_timeout = sse_read_timeout 47 | 48 | async def connect(self) -> None: 49 | """Establish a connection to the MCP implementation.""" 50 | if self._connected: 51 | logger.debug("Already connected to MCP implementation") 52 | return 53 | 54 | logger.debug(f"Connecting to MCP implementation via HTTP/SSE: {self.base_url}") 55 | try: 56 | # Create the SSE connection URL 57 | sse_url = f"{self.base_url}" 58 | 59 | # Create and start the connection manager 60 | self._connection_manager = SseConnectionManager( 61 | sse_url, self.headers, self.timeout, self.sse_read_timeout 62 | ) 63 | read_stream, write_stream = await self._connection_manager.start() 64 | 65 | # Create the client session 66 | self.client = ClientSession(read_stream, write_stream, sampling_callback=None) 67 | await self.client.__aenter__() 68 | 69 | # Mark as connected 70 | self._connected = True 71 | logger.debug( 72 | f"Successfully connected to MCP implementation via HTTP/SSE: {self.base_url}" 73 | ) 74 | 75 | except Exception as e: 76 | logger.error(f"Failed to connect to MCP implementation via HTTP/SSE: {e}") 77 | 78 | # Clean up any resources if connection failed 79 | await self._cleanup_resources() 80 | 81 | # Re-raise the original exception 82 | raise 83 | -------------------------------------------------------------------------------- /mcp_use/connectors/stdio.py: -------------------------------------------------------------------------------- 1 | """ 2 | StdIO connector for MCP implementations. 3 | 4 | This module provides a connector for communicating with MCP implementations 5 | through the standard input/output streams. 6 | """ 7 | 8 | import sys 9 | 10 | from mcp import ClientSession, StdioServerParameters 11 | 12 | from ..logging import logger 13 | from ..task_managers import StdioConnectionManager 14 | from .base import BaseConnector 15 | 16 | 17 | class StdioConnector(BaseConnector): 18 | """Connector for MCP implementations using stdio transport. 19 | 20 | This connector uses the stdio transport to communicate with MCP implementations 21 | that are executed as child processes. It uses a connection manager to handle 22 | the proper lifecycle management of the stdio client. 23 | """ 24 | 25 | def __init__( 26 | self, 27 | command: str = "npx", 28 | args: list[str] | None = None, 29 | env: dict[str, str] | None = None, 30 | errlog=sys.stderr, 31 | ): 32 | """Initialize a new stdio connector. 33 | 34 | Args: 35 | command: The command to execute. 36 | args: Optional command line arguments. 37 | env: Optional environment variables. 38 | errlog: Stream to write error output to. 39 | """ 40 | super().__init__() 41 | self.command = command 42 | self.args = args or [] # Ensure args is never None 43 | self.env = env 44 | self.errlog = errlog 45 | 46 | async def connect(self) -> None: 47 | """Establish a connection to the MCP implementation.""" 48 | if self._connected: 49 | logger.debug("Already connected to MCP implementation") 50 | return 51 | 52 | logger.debug(f"Connecting to MCP implementation: {self.command}") 53 | try: 54 | # Create server parameters 55 | server_params = StdioServerParameters( 56 | command=self.command, args=self.args, env=self.env 57 | ) 58 | 59 | # Create and start the connection manager 60 | self._connection_manager = StdioConnectionManager(server_params, self.errlog) 61 | read_stream, write_stream = await self._connection_manager.start() 62 | 63 | # Create the client session 64 | self.client = ClientSession(read_stream, write_stream, sampling_callback=None) 65 | await self.client.__aenter__() 66 | 67 | # Mark as connected 68 | self._connected = True 69 | logger.debug(f"Successfully connected to MCP implementation: {self.command}") 70 | 71 | except Exception as e: 72 | logger.error(f"Failed to connect to MCP implementation: {e}") 73 | 74 | # Clean up any resources if connection failed 75 | await self._cleanup_resources() 76 | 77 | # Re-raise the original exception 78 | raise 79 | -------------------------------------------------------------------------------- /mcp_use/connectors/websocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebSocket connector for MCP implementations. 3 | 4 | This module provides a connector for communicating with MCP implementations 5 | through WebSocket connections. 6 | """ 7 | 8 | import asyncio 9 | import json 10 | import uuid 11 | from typing import Any 12 | 13 | from mcp.types import Tool 14 | from websockets.client import WebSocketClientProtocol 15 | 16 | from ..logging import logger 17 | from ..task_managers import ConnectionManager, WebSocketConnectionManager 18 | from .base import BaseConnector 19 | 20 | 21 | class WebSocketConnector(BaseConnector): 22 | """Connector for MCP implementations using WebSocket transport. 23 | 24 | This connector uses WebSockets to communicate with remote MCP implementations, 25 | using a connection manager to handle the proper lifecycle management. 26 | """ 27 | 28 | def __init__( 29 | self, 30 | url: str, 31 | auth_token: str | None = None, 32 | headers: dict[str, str] | None = None, 33 | ): 34 | """Initialize a new WebSocket connector. 35 | 36 | Args: 37 | url: The WebSocket URL to connect to. 38 | auth_token: Optional authentication token. 39 | headers: Optional additional headers. 40 | """ 41 | self.url = url 42 | self.auth_token = auth_token 43 | self.headers = headers or {} 44 | if auth_token: 45 | self.headers["Authorization"] = f"Bearer {auth_token}" 46 | 47 | self.ws: WebSocketClientProtocol | None = None 48 | self._connection_manager: ConnectionManager | None = None 49 | self._receiver_task: asyncio.Task | None = None 50 | self.pending_requests: dict[str, asyncio.Future] = {} 51 | self._tools: list[Tool] | None = None 52 | self._connected = False 53 | 54 | async def connect(self) -> None: 55 | """Establish a connection to the MCP implementation.""" 56 | if self._connected: 57 | logger.debug("Already connected to MCP implementation") 58 | return 59 | 60 | logger.debug(f"Connecting to MCP implementation via WebSocket: {self.url}") 61 | try: 62 | # Create and start the connection manager 63 | self._connection_manager = WebSocketConnectionManager(self.url, self.headers) 64 | self.ws = await self._connection_manager.start() 65 | 66 | # Start the message receiver task 67 | self._receiver_task = asyncio.create_task( 68 | self._receive_messages(), name="websocket_receiver_task" 69 | ) 70 | 71 | # Mark as connected 72 | self._connected = True 73 | logger.debug(f"Successfully connected to MCP implementation via WebSocket: {self.url}") 74 | 75 | except Exception as e: 76 | logger.error(f"Failed to connect to MCP implementation via WebSocket: {e}") 77 | 78 | # Clean up any resources if connection failed 79 | await self._cleanup_resources() 80 | 81 | # Re-raise the original exception 82 | raise 83 | 84 | async def _receive_messages(self) -> None: 85 | """Continuously receive and process messages from the WebSocket.""" 86 | if not self.ws: 87 | raise RuntimeError("WebSocket is not connected") 88 | 89 | try: 90 | async for message in self.ws: 91 | # Parse the message 92 | data = json.loads(message) 93 | 94 | # Check if this is a response to a pending request 95 | request_id = data.get("id") 96 | if request_id and request_id in self.pending_requests: 97 | future = self.pending_requests.pop(request_id) 98 | if "result" in data: 99 | future.set_result(data["result"]) 100 | elif "error" in data: 101 | future.set_exception(Exception(data["error"])) 102 | 103 | logger.debug(f"Received response for request {request_id}") 104 | else: 105 | logger.debug(f"Received message: {data}") 106 | except Exception as e: 107 | logger.error(f"Error in WebSocket message receiver: {e}") 108 | # If the websocket connection was closed or errored, 109 | # reject all pending requests 110 | for future in self.pending_requests.values(): 111 | if not future.done(): 112 | future.set_exception(e) 113 | 114 | async def disconnect(self) -> None: 115 | """Close the connection to the MCP implementation.""" 116 | if not self._connected: 117 | logger.debug("Not connected to MCP implementation") 118 | return 119 | 120 | logger.debug("Disconnecting from MCP implementation") 121 | await self._cleanup_resources() 122 | self._connected = False 123 | logger.debug("Disconnected from MCP implementation") 124 | 125 | async def _cleanup_resources(self) -> None: 126 | """Clean up all resources associated with this connector.""" 127 | errors = [] 128 | 129 | # First cancel the receiver task 130 | if self._receiver_task and not self._receiver_task.done(): 131 | try: 132 | logger.debug("Cancelling WebSocket receiver task") 133 | self._receiver_task.cancel() 134 | try: 135 | await self._receiver_task 136 | except asyncio.CancelledError: 137 | logger.debug("WebSocket receiver task cancelled successfully") 138 | except Exception as e: 139 | logger.warning(f"Error during WebSocket receiver task cancellation: {e}") 140 | except Exception as e: 141 | error_msg = f"Error cancelling WebSocket receiver task: {e}" 142 | logger.warning(error_msg) 143 | errors.append(error_msg) 144 | finally: 145 | self._receiver_task = None 146 | 147 | # Reject any pending requests 148 | if self.pending_requests: 149 | logger.debug(f"Rejecting {len(self.pending_requests)} pending requests") 150 | for future in self.pending_requests.values(): 151 | if not future.done(): 152 | future.set_exception(ConnectionError("WebSocket disconnected")) 153 | self.pending_requests.clear() 154 | 155 | # Then stop the connection manager 156 | if self._connection_manager: 157 | try: 158 | logger.debug("Stopping connection manager") 159 | await self._connection_manager.stop() 160 | except Exception as e: 161 | error_msg = f"Error stopping connection manager: {e}" 162 | logger.warning(error_msg) 163 | errors.append(error_msg) 164 | finally: 165 | self._connection_manager = None 166 | self.ws = None 167 | 168 | # Reset tools 169 | self._tools = None 170 | 171 | if errors: 172 | logger.warning(f"Encountered {len(errors)} errors during resource cleanup") 173 | 174 | async def _send_request(self, method: str, params: dict[str, Any] | None = None) -> Any: 175 | """Send a request and wait for a response.""" 176 | if not self.ws: 177 | raise RuntimeError("WebSocket is not connected") 178 | 179 | # Create a request ID 180 | request_id = str(uuid.uuid4()) 181 | 182 | # Create a future to receive the response 183 | future = asyncio.Future() 184 | self.pending_requests[request_id] = future 185 | 186 | # Send the request 187 | await self.ws.send(json.dumps({"id": request_id, "method": method, "params": params or {}})) 188 | 189 | logger.debug(f"Sent request {request_id} method: {method}") 190 | 191 | # Wait for the response 192 | try: 193 | return await future 194 | except Exception as e: 195 | # Remove the request from pending requests 196 | self.pending_requests.pop(request_id, None) 197 | logger.error(f"Error waiting for response to request {request_id}: {e}") 198 | raise 199 | 200 | async def initialize(self) -> dict[str, Any]: 201 | """Initialize the MCP session and return session information.""" 202 | logger.debug("Initializing MCP session") 203 | result = await self._send_request("initialize") 204 | 205 | # Get available tools 206 | tools_result = await self.list_tools() 207 | self._tools = [Tool(**tool) for tool in tools_result] 208 | 209 | logger.debug(f"MCP session initialized with {len(self._tools)} tools") 210 | return result 211 | 212 | async def list_tools(self) -> list[dict[str, Any]]: 213 | """List all available tools from the MCP implementation.""" 214 | logger.debug("Listing tools") 215 | result = await self._send_request("tools/list") 216 | return result.get("tools", []) 217 | 218 | @property 219 | def tools(self) -> list[Tool]: 220 | """Get the list of available tools.""" 221 | if not self._tools: 222 | raise RuntimeError("MCP client is not initialized") 223 | return self._tools 224 | 225 | async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: 226 | """Call an MCP tool with the given arguments.""" 227 | logger.debug(f"Calling tool '{name}' with arguments: {arguments}") 228 | return await self._send_request("tools/call", {"name": name, "arguments": arguments}) 229 | 230 | async def list_resources(self) -> list[dict[str, Any]]: 231 | """List all available resources from the MCP implementation.""" 232 | logger.debug("Listing resources") 233 | result = await self._send_request("resources/list") 234 | return result 235 | 236 | async def read_resource(self, uri: str) -> tuple[bytes, str]: 237 | """Read a resource by URI.""" 238 | logger.debug(f"Reading resource: {uri}") 239 | result = await self._send_request("resources/read", {"uri": uri}) 240 | return result.get("content", b""), result.get("mimeType", "") 241 | 242 | async def request(self, method: str, params: dict[str, Any] | None = None) -> Any: 243 | """Send a raw request to the MCP implementation.""" 244 | logger.debug(f"Sending request: {method} with params: {params}") 245 | return await self._send_request(method, params) 246 | -------------------------------------------------------------------------------- /mcp_use/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger module for mcp_use. 3 | 4 | This module provides a centralized logging configuration for the mcp_use library, 5 | with customizable log levels and formatters. 6 | """ 7 | 8 | import logging 9 | import os 10 | import sys 11 | 12 | from langchain.globals import set_debug as langchain_set_debug 13 | 14 | # Global debug flag - can be set programmatically or from environment 15 | MCP_USE_DEBUG = False 16 | 17 | 18 | class Logger: 19 | """Centralized logger for mcp_use. 20 | 21 | This class provides logging functionality with configurable levels, 22 | formatters, and handlers. 23 | """ 24 | 25 | # Default log format 26 | DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 27 | 28 | # Module-specific loggers 29 | _loggers = {} 30 | 31 | @classmethod 32 | def get_logger(cls, name: str = "mcp_use") -> logging.Logger: 33 | """Get a logger instance for the specified name. 34 | 35 | Args: 36 | name: Logger name, usually the module name (defaults to 'mcp_use') 37 | 38 | Returns: 39 | Configured logger instance 40 | """ 41 | if name in cls._loggers: 42 | return cls._loggers[name] 43 | 44 | # Create new logger 45 | logger = logging.getLogger(name) 46 | cls._loggers[name] = logger 47 | 48 | return logger 49 | 50 | @classmethod 51 | def configure( 52 | cls, 53 | level: int | str = None, 54 | format_str: str | None = None, 55 | log_to_console: bool = True, 56 | log_to_file: str | None = None, 57 | ) -> None: 58 | """Configure the root mcp_use logger. 59 | 60 | Args: 61 | level: Log level (default: DEBUG if MCP_USE_DEBUG is 2, 62 | INFO if MCP_USE_DEBUG is 1, 63 | otherwise WARNING) 64 | format_str: Log format string (default: DEFAULT_FORMAT) 65 | log_to_console: Whether to log to console (default: True) 66 | log_to_file: Path to log file (default: None) 67 | """ 68 | root_logger = cls.get_logger() 69 | 70 | # Set level based on debug settings if not explicitly provided 71 | if level is None: 72 | if MCP_USE_DEBUG == 2: 73 | level = logging.DEBUG 74 | elif MCP_USE_DEBUG == 1: 75 | level = logging.INFO 76 | else: 77 | level = logging.WARNING 78 | elif isinstance(level, str): 79 | level = getattr(logging, level.upper()) 80 | 81 | root_logger.setLevel(level) 82 | 83 | # Clear existing handlers 84 | for handler in root_logger.handlers[:]: 85 | root_logger.removeHandler(handler) 86 | 87 | # Set formatter 88 | formatter = logging.Formatter(format_str or cls.DEFAULT_FORMAT) 89 | 90 | # Add console handler if requested 91 | if log_to_console: 92 | console_handler = logging.StreamHandler(sys.stdout) 93 | console_handler.setFormatter(formatter) 94 | root_logger.addHandler(console_handler) 95 | 96 | # Add file handler if requested 97 | if log_to_file: 98 | # Ensure directory exists 99 | log_dir = os.path.dirname(log_to_file) 100 | if log_dir and not os.path.exists(log_dir): 101 | os.makedirs(log_dir) 102 | 103 | file_handler = logging.FileHandler(log_to_file) 104 | file_handler.setFormatter(formatter) 105 | root_logger.addHandler(file_handler) 106 | 107 | @classmethod 108 | def set_debug(cls, debug_level: int = 2) -> None: 109 | """Set the debug flag and update the log level accordingly. 110 | 111 | Args: 112 | debug_level: Debug level (0=off, 1=info, 2=debug) 113 | """ 114 | global MCP_USE_DEBUG 115 | MCP_USE_DEBUG = debug_level 116 | 117 | # Update log level for existing loggers 118 | if debug_level == 2: 119 | for logger in cls._loggers.values(): 120 | logger.setLevel(logging.DEBUG) 121 | langchain_set_debug(True) 122 | elif debug_level == 1: 123 | for logger in cls._loggers.values(): 124 | logger.setLevel(logging.INFO) 125 | langchain_set_debug(False) 126 | else: 127 | # Reset to default (WARNING) 128 | for logger in cls._loggers.values(): 129 | logger.setLevel(logging.WARNING) 130 | langchain_set_debug(False) 131 | 132 | 133 | # Check environment variable for debug flag 134 | debug_env = os.environ.get("DEBUG", "").lower() 135 | if debug_env == "2": 136 | MCP_USE_DEBUG = 2 137 | elif debug_env == "1": 138 | MCP_USE_DEBUG = 1 139 | 140 | # Configure default logger 141 | Logger.configure() 142 | 143 | logger = Logger.get_logger() 144 | -------------------------------------------------------------------------------- /mcp_use/managers/__init__.py: -------------------------------------------------------------------------------- 1 | from .server_manager import ServerManager 2 | from .tools import ( 3 | ConnectServerTool, 4 | DisconnectServerTool, 5 | GetActiveServerTool, 6 | ListServersTool, 7 | MCPServerTool, 8 | SearchToolsTool, 9 | UseToolFromServerTool, 10 | ) 11 | 12 | __all__ = [ 13 | "ServerManager", 14 | "ListServersTool", 15 | "ConnectServerTool", 16 | "GetActiveServerTool", 17 | "DisconnectServerTool", 18 | "SearchToolsTool", 19 | "MCPServerTool", 20 | "UseToolFromServerTool", 21 | ] 22 | -------------------------------------------------------------------------------- /mcp_use/managers/server_manager.py: -------------------------------------------------------------------------------- 1 | from langchain_core.tools import BaseTool 2 | 3 | from mcp_use.client import MCPClient 4 | from mcp_use.logging import logger 5 | 6 | from ..adapters.base import BaseAdapter 7 | from .tools import ( 8 | ConnectServerTool, 9 | DisconnectServerTool, 10 | GetActiveServerTool, 11 | ListServersTool, 12 | SearchToolsTool, 13 | UseToolFromServerTool, 14 | ) 15 | 16 | 17 | class ServerManager: 18 | """Manages MCP servers and provides tools for server selection and management. 19 | 20 | This class allows an agent to discover and select which MCP server to use, 21 | dynamically activating the tools for the selected server. 22 | """ 23 | 24 | def __init__(self, client: MCPClient, adapter: BaseAdapter) -> None: 25 | """Initialize the server manager. 26 | 27 | Args: 28 | client: The MCPClient instance managing server connections 29 | adapter: The LangChainAdapter for converting MCP tools to LangChain tools 30 | """ 31 | self.client = client 32 | self.adapter = adapter 33 | self.active_server: str | None = None 34 | self.initialized_servers: dict[str, bool] = {} 35 | self._server_tools: dict[str, list[BaseTool]] = {} 36 | 37 | async def initialize(self) -> None: 38 | """Initialize the server manager and prepare server management tools.""" 39 | # Make sure we have server configurations 40 | if not self.client.get_server_names(): 41 | logger.warning("No MCP servers defined in client configuration") 42 | 43 | async def _prefetch_server_tools(self) -> None: 44 | """Pre-fetch tools for all servers to populate the tool search index.""" 45 | servers = self.client.get_server_names() 46 | for server_name in servers: 47 | try: 48 | # Only create session if needed, don't set active 49 | session = None 50 | try: 51 | session = self.client.get_session(server_name) 52 | logger.debug( 53 | f"Using existing session for server '{server_name}' to prefetch tools." 54 | ) 55 | except ValueError: 56 | try: 57 | session = await self.client.create_session(server_name) 58 | logger.debug( 59 | f"Temporarily created session for '{server_name}' to prefetch tools" 60 | ) 61 | except Exception: 62 | logger.warning( 63 | f"Could not create session for '{server_name}' during prefetch" 64 | ) 65 | continue 66 | 67 | # Fetch tools if session is available 68 | if session: 69 | connector = session.connector 70 | tools = await self.adapter._create_tools_from_connectors([connector]) 71 | 72 | # Check if this server's tools have changed 73 | if ( 74 | server_name not in self._server_tools 75 | or self._server_tools[server_name] != tools 76 | ): 77 | self._server_tools[server_name] = tools # Cache tools 78 | self.initialized_servers[server_name] = True # Mark as initialized 79 | logger.debug(f"Prefetched {len(tools)} tools for server '{server_name}'.") 80 | else: 81 | logger.debug( 82 | f"Tools for server '{server_name}' unchanged, using cached version." 83 | ) 84 | except Exception as e: 85 | logger.error(f"Error prefetching tools for server '{server_name}': {e}") 86 | 87 | @property 88 | def tools(self) -> list[BaseTool]: 89 | """Get all server management tools. 90 | 91 | Returns: 92 | list of LangChain tools for server management 93 | """ 94 | return [ 95 | ListServersTool(self), 96 | ConnectServerTool(self), 97 | GetActiveServerTool(self), 98 | DisconnectServerTool(self), 99 | SearchToolsTool(self), 100 | UseToolFromServerTool(self), 101 | ] 102 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_tool import MCPServerTool 2 | from .connect_server import ConnectServerTool 3 | from .disconnect_server import DisconnectServerTool 4 | from .get_active_server import GetActiveServerTool 5 | from .list_servers_tool import ListServersTool 6 | from .search_tools import SearchToolsTool 7 | from .use_tool import UseToolFromServerTool 8 | 9 | __all__ = [ 10 | "MCPServerTool", 11 | "ListServersTool", 12 | "ConnectServerTool", 13 | "GetActiveServerTool", 14 | "DisconnectServerTool", 15 | "SearchToolsTool", 16 | "UseToolFromServerTool", 17 | ] 18 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/base_tool.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from langchain_core.tools import BaseTool 4 | 5 | 6 | class MCPServerTool(BaseTool): 7 | """Base tool for MCP server operations.""" 8 | 9 | name: ClassVar[str] = "mcp_server_tool" 10 | description: ClassVar[str] = "Base tool for MCP server operations." 11 | 12 | def __init__(self, server_manager): 13 | """Initialize with server manager.""" 14 | super().__init__() 15 | self._server_manager = server_manager 16 | 17 | @property 18 | def server_manager(self): 19 | return self._server_manager 20 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/connect_server.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | from mcp_use.logging import logger 6 | 7 | from .base_tool import MCPServerTool 8 | 9 | 10 | class ServerActionInput(BaseModel): 11 | """Base input for server-related actions""" 12 | 13 | server_name: str = Field(description="The name of the MCP server") 14 | 15 | 16 | class ConnectServerTool(MCPServerTool): 17 | """Tool for connecting to a specific MCP server.""" 18 | 19 | name: ClassVar[str] = "connect_to_mcp_server" 20 | description: ClassVar[str] = ( 21 | "Connect to a specific MCP (Model Context Protocol) server to use its " 22 | "tools. Use this tool to connect to a specific server and use its tools." 23 | ) 24 | args_schema: ClassVar[type[BaseModel]] = ServerActionInput 25 | 26 | async def _arun(self, server_name: str) -> str: 27 | """Connect to a specific MCP server.""" 28 | # Check if server exists 29 | servers = self.server_manager.client.get_server_names() 30 | if server_name not in servers: 31 | available = ", ".join(servers) if servers else "none" 32 | return f"Server '{server_name}' not found. Available servers: {available}" 33 | 34 | # If we're already connected to this server, just return 35 | if self.server_manager.active_server == server_name: 36 | return f"Already connected to MCP server '{server_name}'" 37 | 38 | try: 39 | # Create or get session for this server 40 | try: 41 | session = self.server_manager.client.get_session(server_name) 42 | logger.debug(f"Using existing session for server '{server_name}'") 43 | except ValueError: 44 | logger.debug(f"Creating new session for server '{server_name}'") 45 | session = await self.server_manager.client.create_session(server_name) 46 | 47 | # Set as active server 48 | self.server_manager.active_server = server_name 49 | 50 | # Initialize server tools if not already initialized 51 | if server_name not in self.server_manager._server_tools: 52 | connector = session.connector 53 | self.server_manager._server_tools[ 54 | server_name 55 | ] = await self.server_manager.adapter._create_tools_from_connectors([connector]) 56 | self.server_manager.initialized_servers[server_name] = True 57 | 58 | server_tools = self.server_manager._server_tools.get(server_name, []) 59 | num_tools = len(server_tools) 60 | 61 | return f"Connected to MCP server '{server_name}'. {num_tools} tools are now available." 62 | 63 | except Exception as e: 64 | logger.error(f"Error connecting to server '{server_name}': {e}") 65 | return f"Failed to connect to server '{server_name}': {str(e)}" 66 | 67 | def _run(self, server_name: str) -> str: 68 | """Synchronous version that raises a NotImplementedError - use _arun instead.""" 69 | raise NotImplementedError("ConnectServerTool requires async execution. Use _arun instead.") 70 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/disconnect_server.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from pydantic import BaseModel 4 | 5 | from mcp_use.logging import logger 6 | 7 | from .base_tool import MCPServerTool 8 | 9 | 10 | class DisconnectServerInput(BaseModel): 11 | """Empty input for disconnecting from the current server""" 12 | 13 | pass 14 | 15 | 16 | class DisconnectServerTool(MCPServerTool): 17 | """Tool for disconnecting from the currently active MCP server.""" 18 | 19 | name: ClassVar[str] = "disconnect_from_mcp_server" 20 | description: ClassVar[str] = ( 21 | "Disconnect from the currently active MCP (Model Context Protocol) server" 22 | ) 23 | args_schema: ClassVar[type[BaseModel]] = DisconnectServerInput 24 | 25 | def _run(self, **kwargs) -> str: 26 | """Disconnect from the currently active MCP server.""" 27 | if not self.server_manager.active_server: 28 | return "No MCP server is currently active, so there's nothing to disconnect from." 29 | 30 | server_name = self.server_manager.active_server 31 | try: 32 | # Clear the active server 33 | self.server_manager.active_server = None 34 | 35 | # Note: We're not actually closing the session here, just 'deactivating' 36 | # This way we keep the session cache without requiring reconnection if needed again 37 | 38 | return f"Successfully disconnected from MCP server '{server_name}'." 39 | except Exception as e: 40 | logger.error(f"Error disconnecting from server '{server_name}': {e}") 41 | return f"Failed to disconnect from server '{server_name}': {str(e)}" 42 | 43 | async def _arun(self, **kwargs) -> str: 44 | """Async implementation of _run.""" 45 | return self._run(**kwargs) 46 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/get_active_server.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from pydantic import BaseModel 4 | 5 | from .base_tool import MCPServerTool 6 | 7 | 8 | class CurrentServerInput(BaseModel): 9 | """Empty input for checking current server""" 10 | 11 | pass 12 | 13 | 14 | class GetActiveServerTool(MCPServerTool): 15 | """Tool for getting the currently active MCP server.""" 16 | 17 | name: ClassVar[str] = "get_active_mcp_server" 18 | description: ClassVar[str] = "Get the currently active MCP (Model Context Protocol) server" 19 | args_schema: ClassVar[type[BaseModel]] = CurrentServerInput 20 | 21 | def _run(self, **kwargs) -> str: 22 | """Get the currently active MCP server.""" 23 | if not self.server_manager.active_server: 24 | return ( 25 | "No MCP server is currently active. " 26 | "Use connect_to_mcp_server to connect to a server." 27 | ) 28 | return f"Currently active MCP server: {self.server_manager.active_server}" 29 | 30 | async def _arun(self, **kwargs) -> str: 31 | """Async implementation of _run.""" 32 | return self._run(**kwargs) 33 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/list_servers_tool.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from pydantic import BaseModel 4 | 5 | from mcp_use.logging import logger 6 | 7 | from .base_tool import MCPServerTool 8 | 9 | 10 | class listServersInput(BaseModel): 11 | """Empty input for listing available servers""" 12 | 13 | pass 14 | 15 | 16 | class ListServersTool(MCPServerTool): 17 | """Tool for listing available MCP servers.""" 18 | 19 | name: ClassVar[str] = "list_mcp_servers" 20 | description: ClassVar[str] = ( 21 | "Lists all available MCP (Model Context Protocol) servers that can be " 22 | "connected to, along with the tools available on each server. " 23 | "Use this tool to discover servers and see what functionalities they offer." 24 | ) 25 | args_schema: ClassVar[type[BaseModel]] = listServersInput 26 | 27 | def _run(self, **kwargs) -> str: 28 | """List all available MCP servers along with their available tools.""" 29 | servers = self.server_manager.client.get_server_names() 30 | if not servers: 31 | return "No MCP servers are currently defined." 32 | 33 | result = "Available MCP servers:\n" 34 | for i, server_name in enumerate(servers): 35 | active_marker = " (ACTIVE)" if server_name == self.server_manager.active_server else "" 36 | result += f"{i + 1}. {server_name}{active_marker}\n" 37 | 38 | tools: list = [] 39 | try: 40 | # Check cache first 41 | if server_name in self.server_manager._server_tools: 42 | tools = self.server_manager._server_tools[server_name] 43 | tool_count = len(tools) 44 | result += f" {tool_count} tools available for this server\n" 45 | except Exception as e: 46 | logger.error(f"Unexpected error listing tools for server '{server_name}': {e}") 47 | 48 | return result 49 | 50 | async def _arun(self, **kwargs) -> str: 51 | """Async implementation of _run - calls the synchronous version.""" 52 | return self._run(**kwargs) 53 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/search_tools.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from typing import ClassVar 4 | 5 | import numpy as np 6 | from fastembed import TextEmbedding 7 | from langchain_core.tools import BaseTool 8 | from pydantic import BaseModel, Field 9 | 10 | from ...logging import logger 11 | from .base_tool import MCPServerTool 12 | 13 | 14 | class ToolSearchInput(BaseModel): 15 | """Input for searching for tools across MCP servers""" 16 | 17 | query: str = Field(description="The search query to find relevant tools") 18 | top_k: int = Field( 19 | default=100, 20 | description="The maximum number of tools to return (defaults to 100)", 21 | ) 22 | 23 | 24 | class SearchToolsTool(MCPServerTool): 25 | """Tool for searching for tools across all MCP servers using semantic search.""" 26 | 27 | name: ClassVar[str] = "search_mcp_tools" 28 | description: ClassVar[str] = ( 29 | "Search for relevant tools across all MCP servers using semantic search. " 30 | "Provide a description of the tool you think you might need to be able to perform " 31 | "the task you are assigned. Do not be too specific, the search will give you many " 32 | "options. It is important you search for the tool, not for the goal. " 33 | "If your first search doesn't yield relevant results, try using different keywords " 34 | "or more general terms." 35 | ) 36 | args_schema: ClassVar[type[BaseModel]] = ToolSearchInput 37 | 38 | def __init__(self, server_manager): 39 | """Initialize with server manager and create a search tool.""" 40 | super().__init__(server_manager) 41 | self._search_tool = ToolSearchEngine(server_manager=server_manager) 42 | 43 | async def _arun(self, query: str, top_k: int = 100) -> str: 44 | """Search for tools across all MCP servers using semantic search.""" 45 | # Make sure the index is ready, and if not, allow the search_tools method to handle it 46 | # No need to manually check or build the index here as the search_tools method will do that 47 | 48 | # Perform search using our search tool instance 49 | results = await self._search_tool.search_tools( 50 | query, top_k=top_k, active_server=self.server_manager.active_server 51 | ) 52 | return self.format_search_results(results) 53 | 54 | def _run(self, query: str, top_k: int = 100) -> str: 55 | """Synchronous version that raises a NotImplementedError - use _arun instead.""" 56 | raise NotImplementedError("SearchToolsTool requires async execution. Use _arun instead.") 57 | 58 | def format_search_results(self, results: list[tuple[BaseTool, str, float]]) -> str: 59 | """Format search results in a consistent format.""" 60 | 61 | # Only show top_k results 62 | results = results 63 | 64 | formatted_output = "Search results\n\n" 65 | 66 | for i, (tool, server_name, score) in enumerate(results): 67 | # Format score as percentage 68 | if i < 5: 69 | score_pct = f"{score * 100:.1f}%" 70 | logger.info(f"{i}: {tool.name} ({score_pct} match)") 71 | formatted_output += f"[{i + 1}] Tool: {tool.name} ({score_pct} match)\n" 72 | formatted_output += f" Server: {server_name}\n" 73 | formatted_output += f" Description: {tool.description}\n\n" 74 | 75 | # Add footer with information about how to use the results 76 | formatted_output += ( 77 | "\nTo use a tool, connect to the appropriate server first, then invoke the tool." 78 | ) 79 | 80 | return formatted_output 81 | 82 | 83 | class ToolSearchEngine: 84 | """ 85 | Provides semantic search capabilities for MCP tools. 86 | Uses vector similarity for semantic search with optional result caching. 87 | """ 88 | 89 | def __init__(self, server_manager=None, use_caching: bool = True): 90 | """ 91 | Initialize the tool search engine. 92 | 93 | Args: 94 | server_manager: The ServerManager instance to get tools from 95 | use_caching: Whether to cache query results 96 | """ 97 | self.server_manager = server_manager 98 | self.use_caching = use_caching 99 | self.is_indexed = False 100 | 101 | # Initialize model components (loaded on demand) 102 | self.model = None 103 | self.embedding_function = None 104 | 105 | # Data storage 106 | self.tool_embeddings = {} # Maps tool name to embedding vector 107 | self.tools_by_name = {} # Maps tool name to tool instance 108 | self.server_by_tool = {} # Maps tool name to server name 109 | self.tool_texts = {} # Maps tool name to searchable text 110 | self.query_cache = {} # Caches search results by query 111 | 112 | def _load_model(self) -> bool: 113 | """Load the embedding model for semantic search if not already loaded.""" 114 | if self.model is not None: 115 | return True 116 | 117 | try: 118 | self.model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5") 119 | self.embedding_function = lambda texts: list(self.model.embed(texts)) 120 | return True 121 | except Exception: 122 | return False 123 | 124 | async def start_indexing(self) -> None: 125 | """Index the tools from the server manager.""" 126 | if not self.server_manager: 127 | return 128 | 129 | # Get tools from server manager 130 | server_tools = self.server_manager._server_tools 131 | 132 | if not server_tools: 133 | # Try to prefetch tools first 134 | if hasattr(self.server_manager, "_prefetch_server_tools"): 135 | await self.server_manager._prefetch_server_tools() 136 | server_tools = self.server_manager._server_tools 137 | 138 | if server_tools: 139 | await self.index_tools(server_tools) 140 | 141 | async def index_tools(self, server_tools: dict[str, list[BaseTool]]) -> None: 142 | """ 143 | Index all tools from all servers for search. 144 | 145 | Args: 146 | server_tools: dictionary mapping server names to their tools 147 | """ 148 | # Clear previous indexes 149 | self.tool_embeddings = {} 150 | self.tools_by_name = {} 151 | self.server_by_tool = {} 152 | self.tool_texts = {} 153 | self.query_cache = {} 154 | self.is_indexed = False 155 | 156 | # Collect all tools and their descriptions 157 | for server_name, tools in server_tools.items(): 158 | for tool in tools: 159 | # Create text representation for search 160 | tool_text = f"{tool.name}: {tool.description}" 161 | 162 | # Store tool information 163 | self.tools_by_name[tool.name] = tool 164 | self.server_by_tool[tool.name] = server_name 165 | self.tool_texts[tool.name] = tool_text.lower() # For case-insensitive search 166 | 167 | if not self.tool_texts: 168 | return 169 | 170 | # Generate embeddings 171 | if self._load_model(): 172 | tool_names = list(self.tool_texts.keys()) 173 | tool_texts = [self.tool_texts[name] for name in tool_names] 174 | 175 | try: 176 | embeddings = self.embedding_function(tool_texts) 177 | for name, embedding in zip(tool_names, embeddings, strict=True): 178 | self.tool_embeddings[name] = embedding 179 | 180 | # Mark as indexed if we successfully embedded tools 181 | self.is_indexed = len(self.tool_embeddings) > 0 182 | except Exception: 183 | return 184 | 185 | def search(self, query: str, top_k: int = 5) -> list[tuple[BaseTool, str, float]]: 186 | """ 187 | Search for tools that match the query using semantic search. 188 | 189 | Args: 190 | query: The search query 191 | top_k: Number of top results to return 192 | 193 | Returns: 194 | list of tuples containing (tool, server_name, score) 195 | """ 196 | if not self.is_indexed: 197 | return [] 198 | 199 | # Check cache first 200 | cache_key = f"semantic:{query}:{top_k}" 201 | if self.use_caching and cache_key in self.query_cache: 202 | return self.query_cache[cache_key] 203 | 204 | # Ensure model and embeddings exist 205 | if not self._load_model() or not self.tool_embeddings: 206 | return [] 207 | 208 | # Generate embedding for the query 209 | try: 210 | query_embedding = self.embedding_function([query])[0] 211 | except Exception: 212 | return [] 213 | 214 | # Calculate similarity scores 215 | scores = {} 216 | for tool_name, embedding in self.tool_embeddings.items(): 217 | # Calculate cosine similarity 218 | similarity = np.dot(query_embedding, embedding) / ( 219 | np.linalg.norm(query_embedding) * np.linalg.norm(embedding) 220 | ) 221 | scores[tool_name] = float(similarity) 222 | 223 | # Sort by score and get top_k results 224 | sorted_results = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k] 225 | 226 | # Format results 227 | results = [] 228 | for tool_name, score in sorted_results: 229 | tool = self.tools_by_name.get(tool_name) 230 | server_name = self.server_by_tool.get(tool_name) 231 | if tool and server_name: 232 | results.append((tool, server_name, score)) 233 | 234 | # Cache results 235 | if self.use_caching: 236 | self.query_cache[cache_key] = results 237 | 238 | return results 239 | 240 | async def search_tools(self, query: str, top_k: int = 100, active_server: str = None) -> str: 241 | """ 242 | Search for tools across all MCP servers using semantic search. 243 | 244 | Args: 245 | query: The search query to find relevant tools 246 | top_k: Number of top results to return 247 | active_server: Name of the currently active server (for highlighting) 248 | 249 | Returns: 250 | String with formatted search results 251 | """ 252 | # Ensure the index is built or build it 253 | if not self.is_indexed: 254 | # Try to build the index 255 | if self.server_manager and self.server_manager._server_tools: 256 | await self.index_tools(self.server_manager._server_tools) 257 | else: 258 | # If we don't have server_manager or tools, try to index directly 259 | await self.start_indexing() 260 | 261 | # Wait for indexing to complete (maximum 10 seconds) 262 | start_time = time.time() 263 | timeout = 10 # seconds 264 | while not self.is_indexed and (time.time() - start_time) < timeout: 265 | await asyncio.sleep(0.5) 266 | 267 | # If still not indexed, return a friendly message 268 | if not self.is_indexed: 269 | return ( 270 | "I'm still preparing the tool index. Please try your search again in a moment. " 271 | "This usually takes just a few seconds to complete." 272 | ) 273 | 274 | # If the server manager has an active server but it wasn't provided, use it 275 | if ( 276 | active_server is None 277 | and self.server_manager 278 | and hasattr(self.server_manager, "active_server") 279 | ): 280 | active_server = self.server_manager.active_server 281 | 282 | results = self.search(query, top_k=top_k) 283 | if not results: 284 | return ( 285 | "No relevant tools found. The search provided no results. " 286 | "You can try searching again with different keywords. " 287 | "Try using more general terms or focusing on the capability you need." 288 | ) 289 | 290 | # If there's an active server, mark it in the results 291 | if active_server: 292 | # Create a new results list with marked active server 293 | marked_results = [] 294 | for tool, server_name, score in results: 295 | # If this is the active server, add "(ACTIVE)" marker 296 | display_server = ( 297 | f"{server_name} (ACTIVE)" if server_name == active_server else server_name 298 | ) 299 | marked_results.append((tool, display_server, score)) 300 | results = marked_results 301 | 302 | # Format and return the results 303 | return results 304 | -------------------------------------------------------------------------------- /mcp_use/managers/tools/use_tool.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, ClassVar 3 | 4 | from langchain_core.tools import BaseTool 5 | from pydantic import BaseModel, Field 6 | 7 | from mcp_use.logging import logger 8 | 9 | from .base_tool import MCPServerTool 10 | 11 | 12 | class UseToolInput(BaseModel): 13 | """Input for using a tool from a specific server""" 14 | 15 | server_name: str = Field(description="The name of the MCP server containing the tool") 16 | tool_name: str = Field(description="The name of the tool to execute") 17 | tool_input: dict[str, Any] | str = Field( 18 | description="The input to pass to the tool. Can be a dictionary of parameters or a string" 19 | ) 20 | 21 | 22 | class UseToolFromServerTool(MCPServerTool): 23 | """Tool for directly executing a tool from a specific server.""" 24 | 25 | name: ClassVar[str] = "use_tool_from_server" 26 | description: ClassVar[str] = ( 27 | "Execute a specific tool on a specific server without first connecting to it. " 28 | "This is a direct execution shortcut that combines connection and tool execution " 29 | "into a single step. Specify the server name, tool name, and the input to the tool." 30 | ) 31 | args_schema: ClassVar[type[BaseModel]] = UseToolInput 32 | 33 | async def _arun( 34 | self, server_name: str, tool_name: str, tool_input: dict[str, Any] | str 35 | ) -> str: 36 | """Execute a tool from a specific server.""" 37 | # Check if server exists 38 | servers = self.server_manager.client.get_server_names() 39 | if server_name not in servers: 40 | available = ", ".join(servers) if servers else "none" 41 | return f"Server '{server_name}' not found. Available servers: {available}" 42 | 43 | # Connect to the server if not already connected or not the active server 44 | is_connected = server_name == self.server_manager.active_server 45 | 46 | if not is_connected: 47 | try: 48 | # Create or get session for this server 49 | try: 50 | session = self.server_manager.client.get_session(server_name) 51 | logger.debug(f"Using existing session for server '{server_name}'") 52 | except ValueError: 53 | logger.debug(f"Creating new session for server '{server_name}' for tool use") 54 | session = await self.server_manager.client.create_session(server_name) 55 | 56 | # Check if we have tools for this server, if not get them 57 | if server_name not in self.server_manager._server_tools: 58 | connector = session.connector 59 | self.server_manager._server_tools[ 60 | server_name 61 | ] = await self.server_manager.adapter._create_tools_from_connectors([connector]) 62 | self.server_manager.initialized_servers[server_name] = True 63 | except Exception as e: 64 | logger.error(f"Error connecting to server '{server_name}' for tool use: {e}") 65 | return f"Failed to connect to server '{server_name}': {str(e)}" 66 | 67 | # Get tools for the server 68 | server_tools = self.server_manager._server_tools.get(server_name, []) 69 | if not server_tools: 70 | return f"No tools found for server '{server_name}'" 71 | 72 | # Find the requested tool 73 | target_tool = None 74 | for tool in server_tools: 75 | if tool.name == tool_name: 76 | target_tool = tool 77 | break 78 | 79 | if not target_tool: 80 | tool_names = [t.name for t in server_tools] 81 | return ( 82 | f"Tool '{tool_name}' not found on server '{server_name}'. " 83 | f"Available tools: {', '.join(tool_names)}" 84 | ) 85 | 86 | # Execute the tool with the provided input 87 | try: 88 | # Parse the input based on target tool's schema 89 | structured_input = self._parse_tool_input(target_tool, tool_input) 90 | if structured_input is None: 91 | return ( 92 | f"Could not parse input for tool '{tool_name}'." 93 | " Please check the input format and try again." 94 | ) 95 | 96 | # Store the previous active server 97 | previous_active = self.server_manager.active_server 98 | 99 | # Temporarily set this server as active 100 | self.server_manager.active_server = server_name 101 | 102 | # Execute the tool 103 | logger.info( 104 | f"Executing tool '{tool_name}' on server '{server_name}'" 105 | "with input: {structured_input}" 106 | ) 107 | result = await target_tool._arun(**structured_input) 108 | 109 | # Restore the previous active server 110 | self.server_manager.active_server = previous_active 111 | 112 | return result 113 | 114 | except Exception as e: 115 | logger.error(f"Error executing tool '{tool_name}' on server '{server_name}': {e}") 116 | return ( 117 | f"Error executing tool '{tool_name}' on server '{server_name}': {str(e)}. " 118 | f"Make sure the input format is correct for this tool." 119 | ) 120 | 121 | def _parse_tool_input(self, tool: BaseTool, input_data: dict[str, Any] | str) -> dict[str, Any]: 122 | """ 123 | Parse the input data according to the tool's schema. 124 | 125 | Args: 126 | tool: The target tool 127 | input_data: The input data, either a dictionary or a string 128 | 129 | Returns: 130 | A dictionary with properly structured input for the tool 131 | """ 132 | # If input is already a dict, use it directly 133 | if isinstance(input_data, dict): 134 | return input_data 135 | 136 | # Try to parse as JSON first 137 | if isinstance(input_data, str): 138 | try: 139 | return json.loads(input_data) 140 | except json.JSONDecodeError: 141 | pass 142 | 143 | # For string input, we need to determine which parameter name to use 144 | if hasattr(tool, "args_schema") and tool.args_schema: 145 | schema_cls = tool.args_schema 146 | field_names = list(schema_cls.__fields__.keys()) 147 | 148 | # If schema has only one field, use that 149 | if len(field_names) == 1: 150 | return {field_names[0]: input_data} 151 | 152 | # Look for common input field names 153 | for name in field_names: 154 | if name.lower() in ["input", "query", "url", tool.name.lower()]: 155 | return {name: input_data} 156 | 157 | # Default to first field if we can't determine 158 | return {field_names[0]: input_data} 159 | 160 | # If we get here something went wrong 161 | return None 162 | 163 | def _run(self, server_name: str, tool_name: str, tool_input: dict[str, Any] | str) -> str: 164 | """Synchronous version that raises a NotImplementedError.""" 165 | raise NotImplementedError( 166 | "UseToolFromServerTool requires async execution. Use _arun instead." 167 | ) 168 | -------------------------------------------------------------------------------- /mcp_use/session.py: -------------------------------------------------------------------------------- 1 | """ 2 | Session manager for MCP connections. 3 | 4 | This module provides a session manager for MCP connections, 5 | which handles authentication, initialization, and tool discovery. 6 | """ 7 | 8 | from typing import Any 9 | 10 | from .connectors.base import BaseConnector 11 | 12 | 13 | class MCPSession: 14 | """Session manager for MCP connections. 15 | 16 | This class manages the lifecycle of an MCP connection, including 17 | authentication, initialization, and tool discovery. 18 | """ 19 | 20 | def __init__( 21 | self, 22 | connector: BaseConnector, 23 | auto_connect: bool = True, 24 | ) -> None: 25 | """Initialize a new MCP session. 26 | 27 | Args: 28 | connector: The connector to use for communicating with the MCP implementation. 29 | auto_connect: Whether to automatically connect to the MCP implementation. 30 | """ 31 | self.connector = connector 32 | self.session_info: dict[str, Any] | None = None 33 | self.tools: list[dict[str, Any]] = [] 34 | self.auto_connect = auto_connect 35 | 36 | async def __aenter__(self) -> "MCPSession": 37 | """Enter the async context manager. 38 | 39 | Returns: 40 | The session instance. 41 | """ 42 | await self.connect() 43 | return self 44 | 45 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 46 | """Exit the async context manager. 47 | 48 | Args: 49 | exc_type: The exception type, if an exception was raised. 50 | exc_val: The exception value, if an exception was raised. 51 | exc_tb: The exception traceback, if an exception was raised. 52 | """ 53 | await self.disconnect() 54 | 55 | async def connect(self) -> None: 56 | """Connect to the MCP implementation.""" 57 | await self.connector.connect() 58 | 59 | async def disconnect(self) -> None: 60 | """Disconnect from the MCP implementation.""" 61 | await self.connector.disconnect() 62 | 63 | async def initialize(self) -> dict[str, Any]: 64 | """Initialize the MCP session and discover available tools. 65 | 66 | Returns: 67 | The session information returned by the MCP implementation. 68 | """ 69 | # Make sure we're connected 70 | if not self.is_connected and self.auto_connect: 71 | await self.connect() 72 | 73 | # Initialize the session 74 | self.session_info = await self.connector.initialize() 75 | 76 | # Discover available tools 77 | await self.discover_tools() 78 | 79 | return self.session_info 80 | 81 | @property 82 | def is_connected(self) -> bool: 83 | """Check if the connector is connected. 84 | 85 | Returns: 86 | True if the connector is connected, False otherwise. 87 | """ 88 | return hasattr(self.connector, "client") and self.connector.client is not None 89 | 90 | async def discover_tools(self) -> list[dict[str, Any]]: 91 | """Discover available tools from the MCP implementation. 92 | 93 | Returns: 94 | The list of available tools in MCP format. 95 | """ 96 | self.tools = self.connector.tools 97 | return self.tools 98 | 99 | async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: 100 | """Call an MCP tool with the given arguments. 101 | 102 | Args: 103 | name: The name of the tool to call. 104 | arguments: The arguments to pass to the tool. 105 | 106 | Returns: 107 | The result of the tool call. 108 | """ 109 | # Make sure we're connected 110 | if not self.is_connected and self.auto_connect: 111 | await self.connect() 112 | 113 | return await self.connector.call_tool(name, arguments) 114 | -------------------------------------------------------------------------------- /mcp_use/task_managers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connectors for various MCP transports. 3 | 4 | This module provides interfaces for connecting to MCP implementations 5 | through different transport mechanisms. 6 | """ 7 | 8 | from .base import ConnectionManager 9 | from .sse import SseConnectionManager 10 | from .stdio import StdioConnectionManager 11 | from .websocket import WebSocketConnectionManager 12 | 13 | __all__ = [ 14 | "ConnectionManager", 15 | "HttpConnectionManager", 16 | "StdioConnectionManager", 17 | "WebSocketConnectionManager", 18 | "SseConnectionManager", 19 | ] 20 | -------------------------------------------------------------------------------- /mcp_use/task_managers/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connection management for MCP implementations. 3 | 4 | This module provides an abstract base class for different types of connection 5 | managers used in MCP connectors. 6 | """ 7 | 8 | import asyncio 9 | from abc import ABC, abstractmethod 10 | from typing import Generic, TypeVar 11 | 12 | from ..logging import logger 13 | 14 | # Type variable for connection types 15 | T = TypeVar("T") 16 | 17 | 18 | class ConnectionManager(Generic[T], ABC): 19 | """Abstract base class for connection managers. 20 | 21 | This class defines the interface for different types of connection managers 22 | used with MCP connectors. 23 | """ 24 | 25 | def __init__(self): 26 | """Initialize a new connection manager.""" 27 | self._ready_event = asyncio.Event() 28 | self._done_event = asyncio.Event() 29 | self._exception: Exception | None = None 30 | self._connection: T | None = None 31 | self._task: asyncio.Task | None = None 32 | 33 | @abstractmethod 34 | async def _establish_connection(self) -> T: 35 | """Establish the connection. 36 | 37 | This method should be implemented by subclasses to establish 38 | the specific type of connection needed. 39 | 40 | Returns: 41 | The established connection. 42 | 43 | Raises: 44 | Exception: If connection cannot be established. 45 | """ 46 | pass 47 | 48 | @abstractmethod 49 | async def _close_connection(self, connection: T) -> None: 50 | """Close the connection. 51 | 52 | This method should be implemented by subclasses to close 53 | the specific type of connection. 54 | 55 | Args: 56 | connection: The connection to close. 57 | """ 58 | pass 59 | 60 | async def start(self) -> T: 61 | """Start the connection manager and establish a connection. 62 | 63 | Returns: 64 | The established connection. 65 | 66 | Raises: 67 | Exception: If connection cannot be established. 68 | """ 69 | # Reset state 70 | self._ready_event.clear() 71 | self._done_event.clear() 72 | self._exception = None 73 | 74 | # Create a task to establish and maintain the connection 75 | self._task = asyncio.create_task( 76 | self._connection_task(), name=f"{self.__class__.__name__}_task" 77 | ) 78 | 79 | # Wait for the connection to be ready or fail 80 | await self._ready_event.wait() 81 | 82 | # If there was an exception, raise it 83 | if self._exception: 84 | raise self._exception 85 | 86 | # Return the connection 87 | if self._connection is None: 88 | raise RuntimeError("Connection was not established") 89 | return self._connection 90 | 91 | async def stop(self) -> None: 92 | """Stop the connection manager and close the connection.""" 93 | if self._task and not self._task.done(): 94 | # Cancel the task 95 | logger.debug(f"Cancelling {self.__class__.__name__} task") 96 | self._task.cancel() 97 | 98 | # Wait for it to complete 99 | try: 100 | await self._task 101 | except asyncio.CancelledError: 102 | logger.debug(f"{self.__class__.__name__} task cancelled successfully") 103 | except Exception as e: 104 | logger.warning(f"Error stopping {self.__class__.__name__} task: {e}") 105 | 106 | # Wait for the connection to be done 107 | await self._done_event.wait() 108 | logger.debug(f"{self.__class__.__name__} task completed") 109 | 110 | async def _connection_task(self) -> None: 111 | """Run the connection task. 112 | 113 | This task establishes and maintains the connection until cancelled. 114 | """ 115 | logger.debug(f"Starting {self.__class__.__name__} task") 116 | try: 117 | # Establish the connection 118 | self._connection = await self._establish_connection() 119 | logger.debug(f"{self.__class__.__name__} connected successfully") 120 | 121 | # Signal that the connection is ready 122 | self._ready_event.set() 123 | 124 | # Wait indefinitely until cancelled 125 | try: 126 | # This keeps the connection open until cancelled 127 | await asyncio.Event().wait() 128 | except asyncio.CancelledError: 129 | # Expected when stopping 130 | logger.debug(f"{self.__class__.__name__} task received cancellation") 131 | pass 132 | 133 | except Exception as e: 134 | # Store the exception 135 | self._exception = e 136 | logger.error(f"Error in {self.__class__.__name__} task: {e}") 137 | 138 | # Signal that the connection is ready (with error) 139 | self._ready_event.set() 140 | 141 | finally: 142 | # Close the connection if it was established 143 | if self._connection is not None: 144 | try: 145 | await self._close_connection(self._connection) 146 | except Exception as e: 147 | logger.warning(f"Error closing connection in {self.__class__.__name__}: {e}") 148 | self._connection = None 149 | 150 | # Signal that the connection is done 151 | self._done_event.set() 152 | -------------------------------------------------------------------------------- /mcp_use/task_managers/sse.py: -------------------------------------------------------------------------------- 1 | """ 2 | SSE connection management for MCP implementations. 3 | 4 | This module provides a connection manager for SSE-based MCP connections 5 | that ensures proper task isolation and resource cleanup. 6 | """ 7 | 8 | from typing import Any 9 | 10 | from mcp.client.sse import sse_client 11 | 12 | from ..logging import logger 13 | from .base import ConnectionManager 14 | 15 | 16 | class SseConnectionManager(ConnectionManager[tuple[Any, Any]]): 17 | """Connection manager for SSE-based MCP connections. 18 | 19 | This class handles the proper task isolation for sse_client context managers 20 | to prevent the "cancel scope in different task" error. It runs the sse_client 21 | in a dedicated task and manages its lifecycle. 22 | """ 23 | 24 | def __init__( 25 | self, 26 | url: str, 27 | headers: dict[str, str] | None = None, 28 | timeout: float = 5, 29 | sse_read_timeout: float = 60 * 5, 30 | ): 31 | """Initialize a new SSE connection manager. 32 | 33 | Args: 34 | url: The SSE endpoint URL 35 | headers: Optional HTTP headers 36 | timeout: Timeout for HTTP operations in seconds 37 | sse_read_timeout: Timeout for SSE read operations in seconds 38 | """ 39 | super().__init__() 40 | self.url = url 41 | self.headers = headers or {} 42 | self.timeout = timeout 43 | self.sse_read_timeout = sse_read_timeout 44 | self._sse_ctx = None 45 | 46 | async def _establish_connection(self) -> tuple[Any, Any]: 47 | """Establish an SSE connection. 48 | 49 | Returns: 50 | A tuple of (read_stream, write_stream) 51 | 52 | Raises: 53 | Exception: If connection cannot be established. 54 | """ 55 | # Create the context manager 56 | self._sse_ctx = sse_client( 57 | url=self.url, 58 | headers=self.headers, 59 | timeout=self.timeout, 60 | sse_read_timeout=self.sse_read_timeout, 61 | ) 62 | 63 | # Enter the context manager 64 | read_stream, write_stream = await self._sse_ctx.__aenter__() 65 | 66 | # Return the streams 67 | return (read_stream, write_stream) 68 | 69 | async def _close_connection(self, connection: tuple[Any, Any]) -> None: 70 | """Close the SSE connection. 71 | 72 | Args: 73 | connection: The connection to close (ignored, we use the context manager) 74 | """ 75 | if self._sse_ctx: 76 | # Exit the context manager 77 | try: 78 | await self._sse_ctx.__aexit__(None, None, None) 79 | except Exception as e: 80 | logger.warning(f"Error closing SSE context: {e}") 81 | finally: 82 | self._sse_ctx = None 83 | -------------------------------------------------------------------------------- /mcp_use/task_managers/stdio.py: -------------------------------------------------------------------------------- 1 | """ 2 | StdIO connection management for MCP implementations. 3 | 4 | This module provides a connection manager for stdio-based MCP connections 5 | that ensures proper task isolation and resource cleanup. 6 | """ 7 | 8 | import sys 9 | from typing import Any, TextIO 10 | 11 | from mcp import StdioServerParameters 12 | from mcp.client.stdio import stdio_client 13 | 14 | from ..logging import logger 15 | from .base import ConnectionManager 16 | 17 | 18 | class StdioConnectionManager(ConnectionManager[tuple[Any, Any]]): 19 | """Connection manager for stdio-based MCP connections. 20 | 21 | This class handles the proper task isolation for stdio_client context managers 22 | to prevent the "cancel scope in different task" error. It runs the stdio_client 23 | in a dedicated task and manages its lifecycle. 24 | """ 25 | 26 | def __init__( 27 | self, 28 | server_params: StdioServerParameters, 29 | errlog: TextIO = sys.stderr, 30 | ): 31 | """Initialize a new stdio connection manager. 32 | 33 | Args: 34 | server_params: The parameters for the stdio server 35 | errlog: The error log stream 36 | """ 37 | super().__init__() 38 | self.server_params = server_params 39 | self.errlog = errlog 40 | self._stdio_ctx = None 41 | 42 | async def _establish_connection(self) -> tuple[Any, Any]: 43 | """Establish a stdio connection. 44 | 45 | Returns: 46 | A tuple of (read_stream, write_stream) 47 | 48 | Raises: 49 | Exception: If connection cannot be established. 50 | """ 51 | # Create the context manager 52 | self._stdio_ctx = stdio_client(self.server_params, self.errlog) 53 | 54 | # Enter the context manager 55 | read_stream, write_stream = await self._stdio_ctx.__aenter__() 56 | 57 | # Return the streams 58 | return (read_stream, write_stream) 59 | 60 | async def _close_connection(self, connection: tuple[Any, Any]) -> None: 61 | """Close the stdio connection. 62 | 63 | Args: 64 | connection: The connection to close (ignored, we use the context manager) 65 | """ 66 | if self._stdio_ctx: 67 | # Exit the context manager 68 | try: 69 | await self._stdio_ctx.__aexit__(None, None, None) 70 | except Exception as e: 71 | logger.warning(f"Error closing stdio context: {e}") 72 | finally: 73 | self._stdio_ctx = None 74 | -------------------------------------------------------------------------------- /mcp_use/task_managers/websocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebSocket connection management for MCP implementations. 3 | 4 | This module provides a connection manager for WebSocket-based MCP connections. 5 | """ 6 | 7 | import websockets 8 | from websockets.client import ClientConnection 9 | 10 | from ..logging import logger 11 | from .base import ConnectionManager 12 | 13 | 14 | class WebSocketConnectionManager(ConnectionManager[ClientConnection]): 15 | """Connection manager for WebSocket-based MCP connections. 16 | 17 | This class handles the lifecycle of WebSocket connections, ensuring proper 18 | connection establishment and cleanup. 19 | """ 20 | 21 | def __init__( 22 | self, 23 | url: str, 24 | headers: dict[str, str] | None = None, 25 | ): 26 | """Initialize a new WebSocket connection manager. 27 | 28 | Args: 29 | url: The WebSocket URL to connect to 30 | headers: Optional headers to include in the WebSocket connection 31 | """ 32 | super().__init__() 33 | self.url = url 34 | self.headers = headers or {} 35 | 36 | async def _establish_connection(self) -> ClientConnection: 37 | """Establish a WebSocket connection. 38 | 39 | Returns: 40 | The established WebSocket connection 41 | 42 | Raises: 43 | Exception: If connection cannot be established 44 | """ 45 | logger.debug(f"Connecting to WebSocket: {self.url}") 46 | try: 47 | ws = await websockets.connect(self.url, extra_headers=self.headers) 48 | return ws 49 | except Exception as e: 50 | logger.error(f"Failed to connect to WebSocket: {e}") 51 | raise 52 | 53 | async def _close_connection(self, connection: ClientConnection) -> None: 54 | """Close the WebSocket connection. 55 | 56 | Args: 57 | connection: The WebSocket connection to close 58 | """ 59 | try: 60 | logger.debug("Closing WebSocket connection") 61 | await connection.close() 62 | except Exception as e: 63 | logger.warning(f"Error closing WebSocket connection: {e}") 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-use" 3 | version = "1.2.7" 4 | description = "MCP Library for LLMs" 5 | authors = [ 6 | {name = "Pietro Zullo", email = "pietro.zullo@gmail.com"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.11" 10 | license = {text = "MIT"} 11 | classifiers = [ 12 | "Development Status :: 3 - Alpha", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | ] 21 | dependencies = [ 22 | "mcp>=1.5.0", 23 | "langchain>=0.1.0", 24 | "langchain-community>=0.0.10", 25 | "websockets>=12.0", 26 | "aiohttp>=3.9.0", 27 | "pydantic>=2.0.0", 28 | "typing-extensions>=4.8.0", 29 | "jsonschema-pydantic>=0.1.0", 30 | "python-dotenv>=1.0.0", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "pytest>=7.4.0", 36 | "pytest-asyncio>=0.21.0", 37 | "pytest-cov>=4.1.0", 38 | "black>=23.9.0", 39 | "isort>=5.12.0", 40 | "mypy>=1.5.0", 41 | "ruff>=0.1.0", 42 | ] 43 | anthropic = [ 44 | "anthropic>=0.15.0", 45 | ] 46 | openai = [ 47 | "openai>=1.10.0", 48 | ] 49 | search = [ 50 | "fastembed>=0.0.1", 51 | ] 52 | 53 | [build-system] 54 | requires = ["hatchling"] 55 | build-backend = "hatchling.build" 56 | 57 | [tool.pytest.ini_options] 58 | asyncio_mode = "strict" 59 | asyncio_default_fixture_loop_scope = "function" 60 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | asyncio_mode = auto 7 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 100 2 | target-version = "py311" 3 | 4 | [lint] 5 | select = [ 6 | "E", # pycodestyle errors 7 | "F", # pyflakes 8 | "I", # isort 9 | "W", # pycodestyle warnings 10 | "B", # flake8-bugbear 11 | "UP", # pyupgrade 12 | ] 13 | 14 | [lint.per-file-ignores] 15 | "__init__.py" = ["F401"] # Unused imports 16 | "tests/**/*.py" = ["F811", "F401"] # Redefinition in test files 17 | "mcp_use/connectors/websocket.py" = ["C901"] # Function too complex 18 | 19 | [lint.isort] 20 | known-first-party = ["mcp_use"] 21 | 22 | [format] 23 | quote-style = "double" 24 | indent-style = "space" 25 | line-ending = "auto" 26 | -------------------------------------------------------------------------------- /static/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pietrozullo/mcp-use/dddbf5231ea28f83b95bf1a9da577067d4a83963/static/image.jpg -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pytest configuration file. 3 | 4 | This module contains pytest fixtures and configuration for all tests. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import pytest 11 | 12 | # Add the parent directory to the path so tests can import the package 13 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | 16 | # Fixture for mock session 17 | @pytest.fixture 18 | def mock_session(): 19 | """Return a mock session object for testing.""" 20 | from unittest.mock import AsyncMock, MagicMock 21 | 22 | # Create mock connector 23 | connector = MagicMock() 24 | connector.connect = AsyncMock() 25 | connector.disconnect = AsyncMock() 26 | connector.initialize = AsyncMock(return_value={"session_id": "test_session"}) 27 | connector.tools = [{"name": "test_tool"}] 28 | connector.call_tool = AsyncMock(return_value={"result": "success"}) 29 | 30 | return connector 31 | 32 | 33 | # Register marks 34 | def pytest_configure(config): 35 | """Register custom pytest marks.""" 36 | config.addinivalue_line("markers", "slow: mark test as slow running") 37 | config.addinivalue_line("markers", "integration: mark test as integration test") 38 | -------------------------------------------------------------------------------- /tests/unit/test_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the config module. 3 | """ 4 | 5 | import json 6 | import os 7 | import tempfile 8 | import unittest 9 | from unittest.mock import patch 10 | 11 | from mcp_use.config import create_connector_from_config, load_config_file 12 | from mcp_use.connectors import HttpConnector, StdioConnector, WebSocketConnector 13 | 14 | 15 | class TestConfigLoading(unittest.TestCase): 16 | """Tests for configuration loading functions.""" 17 | 18 | def test_load_config_file(self): 19 | """Test loading a configuration file.""" 20 | test_config = {"mcpServers": {"test": {"url": "http://test.com"}}} 21 | 22 | # Create a temporary file with test config 23 | with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp: 24 | json.dump(test_config, temp) 25 | temp_path = temp.name 26 | 27 | try: 28 | # Test loading from file 29 | loaded_config = load_config_file(temp_path) 30 | self.assertEqual(loaded_config, test_config) 31 | finally: 32 | # Clean up temp file 33 | os.unlink(temp_path) 34 | 35 | def test_load_config_file_nonexistent(self): 36 | """Test loading a non-existent file raises FileNotFoundError.""" 37 | with self.assertRaises(FileNotFoundError): 38 | load_config_file("/tmp/nonexistent_file.json") 39 | 40 | 41 | class TestConnectorCreation(unittest.TestCase): 42 | """Tests for connector creation from configuration.""" 43 | 44 | def test_create_http_connector(self): 45 | """Test creating an HTTP connector from config.""" 46 | server_config = { 47 | "url": "http://test.com", 48 | "headers": {"Content-Type": "application/json"}, 49 | "auth_token": "test_token", 50 | } 51 | 52 | connector = create_connector_from_config(server_config) 53 | 54 | self.assertIsInstance(connector, HttpConnector) 55 | self.assertEqual(connector.base_url, "http://test.com") 56 | self.assertEqual( 57 | connector.headers, 58 | {"Content-Type": "application/json", "Authorization": "Bearer test_token"}, 59 | ) 60 | self.assertEqual(connector.auth_token, "test_token") 61 | 62 | def test_create_http_connector_minimal(self): 63 | """Test creating an HTTP connector with minimal config.""" 64 | server_config = {"url": "http://test.com"} 65 | 66 | connector = create_connector_from_config(server_config) 67 | 68 | self.assertIsInstance(connector, HttpConnector) 69 | self.assertEqual(connector.base_url, "http://test.com") 70 | self.assertEqual(connector.headers, {}) 71 | self.assertIsNone(connector.auth_token) 72 | 73 | def test_create_websocket_connector(self): 74 | """Test creating a WebSocket connector from config.""" 75 | server_config = { 76 | "ws_url": "ws://test.com", 77 | "headers": {"Content-Type": "application/json"}, 78 | "auth_token": "test_token", 79 | } 80 | 81 | connector = create_connector_from_config(server_config) 82 | 83 | self.assertIsInstance(connector, WebSocketConnector) 84 | self.assertEqual(connector.url, "ws://test.com") 85 | self.assertEqual( 86 | connector.headers, 87 | {"Content-Type": "application/json", "Authorization": "Bearer test_token"}, 88 | ) 89 | self.assertEqual(connector.auth_token, "test_token") 90 | 91 | def test_create_websocket_connector_minimal(self): 92 | """Test creating a WebSocket connector with minimal config.""" 93 | server_config = {"ws_url": "ws://test.com"} 94 | 95 | connector = create_connector_from_config(server_config) 96 | 97 | self.assertIsInstance(connector, WebSocketConnector) 98 | self.assertEqual(connector.url, "ws://test.com") 99 | self.assertEqual(connector.headers, {}) 100 | self.assertIsNone(connector.auth_token) 101 | 102 | def test_create_stdio_connector(self): 103 | """Test creating a stdio connector from config.""" 104 | server_config = { 105 | "command": "python", 106 | "args": ["-m", "mcp_server"], 107 | "env": {"DEBUG": "1"}, 108 | } 109 | 110 | connector = create_connector_from_config(server_config) 111 | 112 | self.assertIsInstance(connector, StdioConnector) 113 | self.assertEqual(connector.command, "python") 114 | self.assertEqual(connector.args, ["-m", "mcp_server"]) 115 | self.assertEqual(connector.env, {"DEBUG": "1"}) 116 | 117 | def test_create_stdio_connector_minimal(self): 118 | """Test creating a stdio connector with minimal config.""" 119 | server_config = {"command": "python", "args": ["-m", "mcp_server"]} 120 | 121 | connector = create_connector_from_config(server_config) 122 | 123 | self.assertIsInstance(connector, StdioConnector) 124 | self.assertEqual(connector.command, "python") 125 | self.assertEqual(connector.args, ["-m", "mcp_server"]) 126 | self.assertIsNone(connector.env) 127 | 128 | def test_create_connector_invalid_config(self): 129 | """Test creating a connector with invalid config raises ValueError.""" 130 | server_config = {"invalid": "config"} 131 | 132 | with self.assertRaises(ValueError) as context: 133 | create_connector_from_config(server_config) 134 | 135 | self.assertEqual(str(context.exception), "Cannot determine connector type from config") 136 | -------------------------------------------------------------------------------- /tests/unit/test_logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the logging module. 3 | """ 4 | 5 | import logging 6 | import unittest 7 | from unittest.mock import MagicMock, patch 8 | 9 | from mcp_use.logging import Logger, logger 10 | 11 | 12 | class TestLogging(unittest.TestCase): 13 | """Tests for the logging module functionality.""" 14 | 15 | def test_logger_instance(self): 16 | """Test that logger is a properly configured logging.Logger instance.""" 17 | self.assertIsInstance(logger, logging.Logger) 18 | self.assertEqual(logger.name, "mcp_use") 19 | 20 | def test_get_logger(self): 21 | """Test that get_logger returns a logger with the correct name.""" 22 | test_logger = Logger.get_logger("test_module") 23 | self.assertIsInstance(test_logger, logging.Logger) 24 | self.assertEqual(test_logger.name, "test_module") 25 | 26 | def test_get_logger_caching(self): 27 | """Test that get_logger caches loggers.""" 28 | logger1 = Logger.get_logger("test_cache") 29 | logger2 = Logger.get_logger("test_cache") 30 | 31 | self.assertIs(logger1, logger2) 32 | 33 | @patch("logging.StreamHandler") 34 | def test_configure_default(self, mock_stream_handler): 35 | """Test that configure correctly configures logging with default settings.""" 36 | # Set up mocks 37 | mock_handler = MagicMock() 38 | mock_stream_handler.return_value = mock_handler 39 | 40 | # Reset the logger's handlers 41 | root_logger = Logger.get_logger() 42 | for handler in root_logger.handlers[:]: 43 | root_logger.removeHandler(handler) 44 | 45 | # Configure logging with default settings 46 | Logger.configure() 47 | 48 | # Verify stream handler was created 49 | mock_stream_handler.assert_called_once() 50 | 51 | # Verify formatter was set 52 | self.assertIsNotNone(mock_handler.setFormatter.call_args) 53 | formatter = mock_handler.setFormatter.call_args[0][0] 54 | self.assertEqual(formatter._fmt, Logger.DEFAULT_FORMAT) 55 | 56 | @patch("logging.StreamHandler") 57 | def test_configure_debug_level(self, mock_stream_handler): 58 | """Test that configure correctly configures logging with debug level.""" 59 | # Set up mocks 60 | mock_handler = MagicMock() 61 | mock_stream_handler.return_value = mock_handler 62 | 63 | # Reset the logger's handlers 64 | root_logger = Logger.get_logger() 65 | for handler in root_logger.handlers[:]: 66 | root_logger.removeHandler(handler) 67 | 68 | # Configure logging with debug level 69 | Logger.configure(level=logging.DEBUG) 70 | 71 | # Verify level was set 72 | self.assertEqual(root_logger.level, logging.DEBUG) 73 | 74 | # Verify stream handler was created 75 | mock_stream_handler.assert_called_once() 76 | 77 | @patch("logging.StreamHandler") 78 | def test_configure_format(self, mock_stream_handler): 79 | """Test that configure correctly configures logging format.""" 80 | # Set up mocks 81 | mock_handler = MagicMock() 82 | mock_stream_handler.return_value = mock_handler 83 | 84 | # Reset the logger's handlers 85 | root_logger = Logger.get_logger() 86 | for handler in root_logger.handlers[:]: 87 | root_logger.removeHandler(handler) 88 | 89 | # Configure logging with a custom format 90 | test_format = "%(levelname)s - %(message)s" 91 | Logger.configure(format_str=test_format) 92 | 93 | # Verify formatter was set with the custom format 94 | self.assertIsNotNone(mock_handler.setFormatter.call_args) 95 | formatter = mock_handler.setFormatter.call_args[0][0] 96 | self.assertEqual(formatter._fmt, test_format) 97 | 98 | @patch("logging.FileHandler") 99 | def test_configure_file_logging(self, mock_file_handler): 100 | """Test configuring logging to a file.""" 101 | # Set up mocks 102 | mock_handler = MagicMock() 103 | mock_file_handler.return_value = mock_handler 104 | 105 | # Reset the logger's handlers 106 | root_logger = Logger.get_logger() 107 | for handler in root_logger.handlers[:]: 108 | root_logger.removeHandler(handler) 109 | 110 | # Configure logging with a file 111 | Logger.configure(log_to_file="/tmp/test.log") 112 | 113 | # Verify FileHandler was created 114 | mock_file_handler.assert_called_once_with("/tmp/test.log") 115 | 116 | # Verify formatter was set 117 | self.assertIsNotNone(mock_handler.setFormatter.call_args) 118 | -------------------------------------------------------------------------------- /tests/unit/test_session.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the MCPSession class. 3 | """ 4 | 5 | import unittest 6 | from unittest import IsolatedAsyncioTestCase 7 | from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch 8 | 9 | from mcp_use.session import MCPSession 10 | 11 | 12 | class TestMCPSessionInitialization(unittest.TestCase): 13 | """Tests for MCPSession initialization.""" 14 | 15 | def test_init_default(self): 16 | """Test initialization with default parameters.""" 17 | connector = MagicMock() 18 | session = MCPSession(connector) 19 | 20 | self.assertEqual(session.connector, connector) 21 | self.assertIsNone(session.session_info) 22 | self.assertEqual(session.tools, []) 23 | self.assertTrue(session.auto_connect) 24 | 25 | def test_init_with_auto_connect_false(self): 26 | """Test initialization with auto_connect set to False.""" 27 | connector = MagicMock() 28 | session = MCPSession(connector, auto_connect=False) 29 | 30 | self.assertEqual(session.connector, connector) 31 | self.assertIsNone(session.session_info) 32 | self.assertEqual(session.tools, []) 33 | self.assertFalse(session.auto_connect) 34 | 35 | 36 | class TestMCPSessionConnection(IsolatedAsyncioTestCase): 37 | """Tests for MCPSession connection methods.""" 38 | 39 | def setUp(self): 40 | """Set up a session with a mock connector for each test.""" 41 | self.connector = MagicMock() 42 | self.connector.connect = AsyncMock() 43 | self.connector.disconnect = AsyncMock() 44 | 45 | # By default, the connector is not connected 46 | type(self.connector).client = PropertyMock(return_value=None) 47 | 48 | self.session = MCPSession(self.connector) 49 | 50 | async def test_connect(self): 51 | """Test connecting to the MCP implementation.""" 52 | await self.session.connect() 53 | self.connector.connect.assert_called_once() 54 | 55 | async def test_disconnect(self): 56 | """Test disconnecting from the MCP implementation.""" 57 | await self.session.disconnect() 58 | self.connector.disconnect.assert_called_once() 59 | 60 | async def test_async_context_manager(self): 61 | """Test using the session as an async context manager.""" 62 | async with self.session as session: 63 | self.assertEqual(session, self.session) 64 | self.connector.connect.assert_called_once() 65 | 66 | self.connector.disconnect.assert_called_once() 67 | 68 | async def test_is_connected_property(self): 69 | """Test the is_connected property.""" 70 | # Test when not connected 71 | self.assertFalse(self.session.is_connected) 72 | 73 | # Test when connected 74 | type(self.connector).client = PropertyMock(return_value=MagicMock()) 75 | self.assertTrue(self.session.is_connected) 76 | 77 | 78 | class TestMCPSessionOperations(IsolatedAsyncioTestCase): 79 | """Tests for MCPSession operations.""" 80 | 81 | def setUp(self): 82 | """Set up a session with a mock connector for each test.""" 83 | self.connector = MagicMock() 84 | self.connector.connect = AsyncMock() 85 | self.connector.disconnect = AsyncMock() 86 | self.connector.initialize = AsyncMock(return_value={"session_id": "test_session"}) 87 | self.connector.tools = [{"name": "test_tool"}] 88 | self.connector.call_tool = AsyncMock(return_value={"result": "success"}) 89 | 90 | # By default, the connector is not connected 91 | type(self.connector).client = PropertyMock(return_value=None) 92 | 93 | self.session = MCPSession(self.connector) 94 | 95 | async def test_initialize(self): 96 | """Test initializing the session.""" 97 | # Test initialization when not connected 98 | result = await self.session.initialize() 99 | 100 | # Verify connect was called since auto_connect is True 101 | self.connector.connect.assert_called_once() 102 | self.connector.initialize.assert_called_once() 103 | 104 | # Verify session_info was set 105 | self.assertEqual(self.session.session_info, {"session_id": "test_session"}) 106 | self.assertEqual(result, {"session_id": "test_session"}) 107 | 108 | # Verify tools were discovered 109 | self.assertEqual(self.session.tools, [{"name": "test_tool"}]) 110 | 111 | async def test_initialize_already_connected(self): 112 | """Test initializing the session when already connected.""" 113 | # Set up the connector to indicate it's already connected 114 | type(self.connector).client = PropertyMock(return_value=MagicMock()) 115 | 116 | # Test initialization when already connected 117 | await self.session.initialize() 118 | 119 | # Verify connect was not called since already connected 120 | self.connector.connect.assert_not_called() 121 | self.connector.initialize.assert_called_once() 122 | 123 | async def test_discover_tools(self): 124 | """Test discovering available tools.""" 125 | tools = await self.session.discover_tools() 126 | 127 | # Verify tools were set correctly 128 | self.assertEqual(tools, [{"name": "test_tool"}]) 129 | self.assertEqual(self.session.tools, [{"name": "test_tool"}]) 130 | 131 | async def test_call_tool_connected(self): 132 | """Test calling a tool when already connected.""" 133 | # Set up the connector to indicate it's already connected 134 | type(self.connector).client = PropertyMock(return_value=MagicMock()) 135 | 136 | # Call the tool 137 | result = await self.session.call_tool("test_tool", {"param": "value"}) 138 | 139 | # Verify the connector's call_tool method was called with the right arguments 140 | self.connector.call_tool.assert_called_once_with("test_tool", {"param": "value"}) 141 | 142 | # Verify the result is correct 143 | self.assertEqual(result, {"result": "success"}) 144 | 145 | # Verify connect was not called since already connected 146 | self.connector.connect.assert_not_called() 147 | 148 | async def test_call_tool_not_connected(self): 149 | """Test calling a tool when not connected.""" 150 | # Call the tool 151 | result = await self.session.call_tool("test_tool", {"param": "value"}) 152 | 153 | # Verify connect was called since auto_connect is True 154 | self.connector.connect.assert_called_once() 155 | 156 | # Verify the connector's call_tool method was called with the right arguments 157 | self.connector.call_tool.assert_called_once_with("test_tool", {"param": "value"}) 158 | 159 | # Verify the result is correct 160 | self.assertEqual(result, {"result": "success"}) 161 | 162 | async def test_call_tool_with_auto_connect_false(self): 163 | """Test calling a tool with auto_connect set to False.""" 164 | # Create a session with auto_connect=False 165 | session = MCPSession(self.connector, auto_connect=False) 166 | 167 | # Set up the connector to indicate it's already connected 168 | type(self.connector).client = PropertyMock(return_value=MagicMock()) 169 | 170 | # Call the tool 171 | await session.call_tool("test_tool", {"param": "value"}) 172 | 173 | # Verify connect was not called since auto_connect is False 174 | self.connector.connect.assert_not_called() 175 | -------------------------------------------------------------------------------- /tests/unit/test_stdio_connector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the StdioConnector class. 3 | """ 4 | 5 | import sys 6 | from unittest.mock import AsyncMock, MagicMock, Mock, patch 7 | 8 | import pytest 9 | from mcp.types import CallToolResult, ListResourcesResult, ReadResourceResult, Tool 10 | 11 | from mcp_use.connectors.stdio import StdioConnector 12 | from mcp_use.task_managers.stdio import StdioConnectionManager 13 | 14 | 15 | @pytest.fixture(autouse=True) 16 | def mock_logger(): 17 | """Mock the logger to prevent errors during tests.""" 18 | with patch("mcp_use.connectors.base.logger") as mock_logger: 19 | yield mock_logger 20 | 21 | 22 | class TestStdioConnectorInitialization: 23 | """Tests for StdioConnector initialization.""" 24 | 25 | def test_init_default(self): 26 | """Test initialization with default parameters.""" 27 | connector = StdioConnector() 28 | 29 | assert connector.command == "npx" 30 | assert connector.args == [] 31 | assert connector.env is None 32 | assert connector.errlog == sys.stderr 33 | assert connector.client is None 34 | assert connector._connection_manager is None 35 | assert connector._tools is None 36 | assert connector._connected is False 37 | 38 | def test_init_with_params(self): 39 | """Test initialization with custom parameters.""" 40 | command = "custom-command" 41 | args = ["--arg1", "--arg2"] 42 | env = {"ENV_VAR": "value"} 43 | errlog = Mock() 44 | 45 | connector = StdioConnector(command, args, env, errlog) 46 | 47 | assert connector.command == command 48 | assert connector.args == args 49 | assert connector.env == env 50 | assert connector.errlog == errlog 51 | assert connector.client is None 52 | assert connector._connection_manager is None 53 | assert connector._tools is None 54 | assert connector._connected is False 55 | 56 | 57 | class TestStdioConnectorConnection: 58 | """Tests for StdioConnector connection methods.""" 59 | 60 | @pytest.mark.asyncio 61 | @patch("mcp_use.connectors.stdio.StdioConnectionManager") 62 | @patch("mcp_use.connectors.stdio.ClientSession") 63 | @patch("mcp_use.connectors.stdio.logger") 64 | async def test_connect(self, mock_stdio_logger, mock_client_session, mock_connection_manager): 65 | """Test connecting to the MCP implementation.""" 66 | # Setup mocks 67 | mock_manager_instance = Mock(spec=StdioConnectionManager) 68 | mock_manager_instance.start = AsyncMock(return_value=("read_stream", "write_stream")) 69 | mock_connection_manager.return_value = mock_manager_instance 70 | 71 | mock_client_instance = Mock() 72 | mock_client_instance.__aenter__ = AsyncMock() 73 | mock_client_session.return_value = mock_client_instance 74 | 75 | # Create connector and connect 76 | connector = StdioConnector(command="test-command", args=["--test"]) 77 | await connector.connect() 78 | 79 | # Verify connection manager creation 80 | mock_connection_manager.assert_called_once() 81 | mock_manager_instance.start.assert_called_once() 82 | 83 | # Verify client session creation 84 | mock_client_session.assert_called_once_with( 85 | "read_stream", "write_stream", sampling_callback=None 86 | ) 87 | mock_client_instance.__aenter__.assert_called_once() 88 | 89 | # Verify state 90 | assert connector._connected is True 91 | assert connector.client == mock_client_instance 92 | assert connector._connection_manager == mock_manager_instance 93 | 94 | @pytest.mark.asyncio 95 | @patch("mcp_use.connectors.stdio.logger") 96 | async def test_connect_already_connected(self, mock_stdio_logger): 97 | """Test connecting when already connected.""" 98 | connector = StdioConnector() 99 | connector._connected = True 100 | 101 | await connector.connect() 102 | 103 | # Verify no connection established since already connected 104 | assert connector._connection_manager is None 105 | assert connector.client is None 106 | 107 | @pytest.mark.asyncio 108 | @patch("mcp_use.connectors.stdio.StdioConnectionManager") 109 | @patch("mcp_use.connectors.stdio.ClientSession") 110 | @patch("mcp_use.connectors.stdio.logger") 111 | @patch("mcp_use.connectors.base.logger") 112 | async def test_connect_error( 113 | self, 114 | mock_base_logger, 115 | mock_stdio_logger, 116 | mock_client_session, 117 | mock_connection_manager, 118 | ): 119 | """Test connection error handling.""" 120 | # Setup mocks to raise an exception 121 | mock_manager_instance = Mock(spec=StdioConnectionManager) 122 | mock_manager_instance.start = AsyncMock(side_effect=Exception("Connection error")) 123 | mock_connection_manager.return_value = mock_manager_instance 124 | 125 | mock_manager_instance.stop = AsyncMock() 126 | 127 | # Create connector and attempt to connect 128 | connector = StdioConnector() 129 | 130 | # Expect the exception to be re-raised 131 | with pytest.raises(Exception, match="Connection error"): 132 | await connector.connect() 133 | 134 | # Verify resources were cleaned up 135 | assert connector._connected is False 136 | assert connector.client is None 137 | 138 | # Mock should be called to clean up resources 139 | mock_manager_instance.stop.assert_called_once() 140 | 141 | @pytest.mark.asyncio 142 | async def test_disconnect_not_connected(self): 143 | """Test disconnecting when not connected.""" 144 | connector = StdioConnector() 145 | connector._connected = False 146 | 147 | await connector.disconnect() 148 | 149 | # Should do nothing since not connected 150 | assert connector._connected is False 151 | 152 | @pytest.mark.asyncio 153 | async def test_disconnect(self): 154 | """Test disconnecting from MCP implementation.""" 155 | connector = StdioConnector() 156 | connector._connected = True 157 | 158 | # Mock the _cleanup_resources method to replace the actual method 159 | connector._cleanup_resources = AsyncMock() 160 | 161 | # Disconnect 162 | await connector.disconnect() 163 | 164 | # Verify _cleanup_resources was called 165 | connector._cleanup_resources.assert_called_once() 166 | 167 | # Verify state 168 | assert connector._connected is False 169 | 170 | 171 | class TestStdioConnectorOperations: 172 | """Tests for StdioConnector operations.""" 173 | 174 | @pytest.mark.asyncio 175 | async def test_initialize(self): 176 | """Test initializing the MCP session.""" 177 | connector = StdioConnector() 178 | 179 | # Setup mocks 180 | mock_client = Mock() 181 | mock_client.initialize = AsyncMock(return_value={"status": "success"}) 182 | mock_client.list_tools = AsyncMock(return_value=Mock(tools=[Mock(spec=Tool)])) 183 | connector.client = mock_client 184 | 185 | # Initialize 186 | result = await connector.initialize() 187 | 188 | # Verify 189 | mock_client.initialize.assert_called_once() 190 | mock_client.list_tools.assert_called_once() 191 | 192 | assert result == {"status": "success"} 193 | assert connector._tools is not None 194 | assert len(connector._tools) == 1 195 | 196 | @pytest.mark.asyncio 197 | async def test_initialize_no_client(self): 198 | """Test initializing without a client.""" 199 | connector = StdioConnector() 200 | connector.client = None 201 | 202 | # Expect RuntimeError 203 | with pytest.raises(RuntimeError, match="MCP client is not connected"): 204 | await connector.initialize() 205 | 206 | def test_tools_property(self): 207 | """Test the tools property.""" 208 | connector = StdioConnector() 209 | mock_tools = [Mock(spec=Tool)] 210 | connector._tools = mock_tools 211 | 212 | # Get tools 213 | tools = connector.tools 214 | 215 | assert tools == mock_tools 216 | 217 | def test_tools_property_not_initialized(self): 218 | """Test the tools property when not initialized.""" 219 | connector = StdioConnector() 220 | connector._tools = None 221 | 222 | # Expect RuntimeError 223 | with pytest.raises(RuntimeError, match="MCP client is not initialized"): 224 | _ = connector.tools 225 | 226 | @pytest.mark.asyncio 227 | async def test_call_tool(self): 228 | """Test calling an MCP tool.""" 229 | connector = StdioConnector() 230 | mock_client = Mock() 231 | mock_result = Mock(spec=CallToolResult) 232 | mock_client.call_tool = AsyncMock(return_value=mock_result) 233 | connector.client = mock_client 234 | 235 | # Call tool 236 | tool_name = "test_tool" 237 | arguments = {"param": "value"} 238 | result = await connector.call_tool(tool_name, arguments) 239 | 240 | # Verify 241 | mock_client.call_tool.assert_called_once_with(tool_name, arguments) 242 | assert result == mock_result 243 | 244 | @pytest.mark.asyncio 245 | async def test_call_tool_no_client(self): 246 | """Test calling a tool without a client.""" 247 | connector = StdioConnector() 248 | connector.client = None 249 | 250 | # Expect RuntimeError 251 | with pytest.raises(RuntimeError, match="MCP client is not connected"): 252 | await connector.call_tool("test_tool", {}) 253 | 254 | @pytest.mark.asyncio 255 | async def test_list_resources(self): 256 | """Test listing resources.""" 257 | connector = StdioConnector() 258 | mock_client = Mock() 259 | mock_result = MagicMock() 260 | mock_client.list_resources = AsyncMock(return_value=mock_result) 261 | connector.client = mock_client 262 | 263 | # List resources 264 | result = await connector.list_resources() 265 | 266 | # Verify 267 | mock_client.list_resources.assert_called_once() 268 | assert result == mock_result 269 | 270 | @pytest.mark.asyncio 271 | async def test_list_resources_no_client(self): 272 | """Test listing resources without a client.""" 273 | connector = StdioConnector() 274 | connector.client = None 275 | 276 | # Expect RuntimeError 277 | with pytest.raises(RuntimeError, match="MCP client is not connected"): 278 | await connector.list_resources() 279 | 280 | @pytest.mark.asyncio 281 | async def test_read_resource(self): 282 | """Test reading a resource.""" 283 | connector = StdioConnector() 284 | mock_client = Mock() 285 | mock_result = Mock(spec=ReadResourceResult) 286 | mock_result.content = b"test content" 287 | mock_result.mimeType = "text/plain" 288 | mock_client.read_resource = AsyncMock(return_value=mock_result) 289 | connector.client = mock_client 290 | 291 | # Read resource 292 | uri = "test_uri" 293 | content, mime_type = await connector.read_resource(uri) 294 | 295 | # Verify 296 | mock_client.read_resource.assert_called_once_with(uri) 297 | assert content == b"test content" 298 | assert mime_type == "text/plain" 299 | 300 | @pytest.mark.asyncio 301 | async def test_read_resource_no_client(self): 302 | """Test reading a resource without a client.""" 303 | connector = StdioConnector() 304 | connector.client = None 305 | 306 | # Expect RuntimeError 307 | with pytest.raises(RuntimeError, match="MCP client is not connected"): 308 | await connector.read_resource("test_uri") 309 | 310 | @pytest.mark.asyncio 311 | async def test_request(self): 312 | """Test sending a raw request.""" 313 | connector = StdioConnector() 314 | mock_client = Mock() 315 | mock_result = {"result": "success"} 316 | mock_client.request = AsyncMock(return_value=mock_result) 317 | connector.client = mock_client 318 | 319 | # Send request 320 | method = "test_method" 321 | params = {"param": "value"} 322 | result = await connector.request(method, params) 323 | 324 | # Verify 325 | mock_client.request.assert_called_once_with({"method": method, "params": params}) 326 | assert result == mock_result 327 | 328 | @pytest.mark.asyncio 329 | async def test_request_no_params(self): 330 | """Test sending a raw request without params.""" 331 | connector = StdioConnector() 332 | mock_client = Mock() 333 | mock_result = {"result": "success"} 334 | mock_client.request = AsyncMock(return_value=mock_result) 335 | connector.client = mock_client 336 | 337 | # Send request without params 338 | method = "test_method" 339 | result = await connector.request(method) 340 | 341 | # Verify 342 | mock_client.request.assert_called_once_with({"method": method, "params": {}}) 343 | assert result == mock_result 344 | 345 | @pytest.mark.asyncio 346 | async def test_request_no_client(self): 347 | """Test sending a raw request without a client.""" 348 | connector = StdioConnector() 349 | connector.client = None 350 | 351 | # Expect RuntimeError 352 | with pytest.raises(RuntimeError, match="MCP client is not connected"): 353 | await connector.request("test_method") 354 | --------------------------------------------------------------------------------