├── .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 |
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 |
7 |
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 |
8 |
--------------------------------------------------------------------------------
/docs/logo/light.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------