diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
deleted file mode 100644
index af5aea9..0000000
--- a/.github/workflows/changelog.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-name: "Update Changelogs"
-
-on:
- release:
- types: [released]
-
-jobs:
- update:
- runs-on: ubuntu-latest
-
- permissions:
- # Give the default GITHUB_TOKEN write permission to commit and push the
- # updated CHANGELOG back to the repository.
- # https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/
- contents: write
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- ref: ${{ github.event.release.target_commitish }}
- fetch-depth: 0
- token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Update Changelog
- uses: stefanzweifel/changelog-updater-action@v1
- with:
- latest-version: ${{ github.event.release.tag_name }}
- release-notes: ${{ github.event.release.body }}
-
- - name: Update Docs Changelog
- run: |
- # Get current date in the required format
- CURRENT_DATE=$(date +"%Y‑%m‑%d")
-
- # Create the new changelog entry
- NEW_ENTRY="
- ## ${{ github.event.release.tag_name }}
- ${{ github.event.release.body }}
-
-
- "
-
- # Read the current changelog and insert the new entry after the front matter
- python -c "
- import re
-
- # Read the current changelog
- with open('docs/changelog.mdx', 'r') as f:
- content = f.read()
-
- # Find the end of the front matter
- front_matter_end = content.find('---', content.find('---') + 1) + 3
-
- # Split content into front matter and body
- front_matter = content[:front_matter_end]
- body = content[front_matter_end:]
-
- # Create new entry
- new_entry = '''$NEW_ENTRY'''
-
- # Combine and write back
- new_content = front_matter + '\n\n' + new_entry + body.lstrip()
-
- with open('docs/changelog.mdx', 'w') as f:
- f.write(new_content)
- "
-
- - name: Commit updated CHANGELOG
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- branch: ${{ github.event.release.target_commitish }}
- commit_message: Update CHANGELOG and docs changelog
- file_pattern: CHANGELOG.md docs/changelog.mdx
diff --git a/docs/changelog/1_3_11.mdx b/docs/changelog/1_3_11.mdx
index 144cf3f..9511a42 100644
--- a/docs/changelog/1_3_11.mdx
+++ b/docs/changelog/1_3_11.mdx
@@ -1,7 +1,7 @@
---
title: "Version 1.3.11"
description: "Enhanced error handling, authorization features, and critical bug fixes"
-mode: "center"
+icon: "key"
---

diff --git a/docs/changelog/changelog.mdx b/docs/changelog/changelog.mdx
index dbea48b..bb92175 100644
--- a/docs/changelog/changelog.mdx
+++ b/docs/changelog/changelog.mdx
@@ -1,11 +1,11 @@
---
title: "Library Updates"
description: "New updates and improvements"
-mode: "center"
+icon: "history"
---
-
- ## v1.3.11
+
+ ## Auth Revolution
- **New**: Complete OAuth2 and Bearer token authentication framework ([#149](https://github.com/pietrozullo/mcp-use/pull/149))
- Fix: HTTP connector error handling with better diagnostics ([#279](https://github.com/pietrozullo/mcp-use/pull/279))
- Fix: Accept empty tool results without validation errors ([#273](https://github.com/pietrozullo/mcp-use/pull/273))
@@ -15,16 +15,16 @@ mode: "center"
- Fix: Subprocess cleanup for multiple MCP clients to prevent memory leaks ([#231](https://github.com/pietrozullo/mcp-use/pull/231))
-
- ## v1.3.3
+
+ ## Stability Boost
- Set default logging level to info
- Fix: prevent double async cleanup and event loop errors in astream
- Fix: Raise import error for fastembed, server manager does not silently fail
- Fix: search tools tuple unpacking error
-
- ## v1.3.1
+
+ ## Stream Power
- Remove client options for easier usage
- Add streamable HTTP support
- Fix websocket error positional arguments (headers were missing)
@@ -32,40 +32,40 @@ mode: "center"
- Add CLAUDE.md for development guidance
-
- ## v1.3.0
+
+ ## Sandbox Magic
- Added optional E2B sandbox execution so MCP servers can run in secure cloud sandboxes.
- `MCPAgent.astream()` now lets you stream results **and** automatically log full conversation history.
-
- ## v1.2.13
+
+ ## Resource Discovery
- Alpha support for **Resources** & **Prompts** exposed by remote servers.
- Routine version bump and stability tweaks across task / connection managers.
-
- ## v1.2.10
+
+ ## Emergency Patch
- Hot‑fix: patched **FastEmbed** import failure that could break vector search.
-
- ## v1.1.5
+
+ ## Polish Pass
- Maintenance release – internal refactors, doc clean‑ups, and incremental API polish.
-
- ## v1.0.1
+
+ ## Transport Layer
- Introduced HTTP transport layer and dynamic multi‑server selection.
-
- ## v1.0.0
+
+ ## Stable Launch
- First stable release of the unified Python client after the 0.0.x preview series.
-
- ## v0.0.6
+
+ ## Public Preview
- Initial public preview published to PyPI; automated publish workflow enabled.
diff --git a/tests/unit/test_agent.py b/tests/unit/test_agent.py
new file mode 100644
index 0000000..ba8cc98
--- /dev/null
+++ b/tests/unit/test_agent.py
@@ -0,0 +1,234 @@
+"""
+Unit tests for the MCPAgent class.
+"""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from langchain.schema import HumanMessage
+from langchain_core.agents import AgentFinish
+
+from mcp_use.agents.mcpagent import MCPAgent
+from mcp_use.client import MCPClient
+from mcp_use.connectors.base import BaseConnector
+
+
+class TestMCPAgentInitialization:
+ """Tests for MCPAgent initialization"""
+
+ def _mock_llm(self):
+ llm = MagicMock()
+ llm._llm_type = "test-provider"
+ llm._identifying_params = {"model": "test-model"}
+ return llm
+
+ def test_init_with_llm_and_client(self):
+ """Initializing locally with LLM and client."""
+ llm = self._mock_llm()
+ client = MagicMock(spec=MCPClient)
+
+ agent = MCPAgent(llm=llm, client=client)
+
+ assert agent.llm is llm
+ assert agent.client is client
+ assert agent._is_remote is False
+ assert agent._initialized is False
+ assert agent._agent_executor is None
+ assert isinstance(agent.tools_used_names, list)
+
+ def test_init_requires_llm_for_local(self):
+ """Omitting LLM for local execution raises ValueError."""
+ with pytest.raises(ValueError) as exc:
+ MCPAgent(client=MagicMock(spec=MCPClient))
+ assert "llm is required for local execution" in str(exc.value)
+
+ def test_init_requires_client_or_connectors(self):
+ """LLM present but no client/connectors raises ValueError."""
+ llm = self._mock_llm()
+ with pytest.raises(ValueError) as exc:
+ MCPAgent(llm=llm)
+ assert "Either client or connector must be provided" in str(exc.value)
+
+ def test_init_with_connectors_only(self):
+ """LLM with connectors initializes without client."""
+ llm = self._mock_llm()
+ connector = MagicMock(spec=BaseConnector)
+
+ agent = MCPAgent(llm=llm, connectors=[connector])
+
+ assert agent.client is None
+ assert agent.connectors == [connector]
+ assert agent._is_remote is False
+
+ def test_server_manager_requires_client(self):
+ """Using server manager without client raises ValueError."""
+ llm = self._mock_llm()
+ with pytest.raises(ValueError) as exc:
+ MCPAgent(llm=llm, connectors=[MagicMock(spec=BaseConnector)], use_server_manager=True)
+ assert "Client must be provided when using server manager" in str(exc.value)
+
+ def test_init_remote_mode_with_agent_id(self):
+ """Providing agent_id enables remote mode and skips local requirements."""
+ with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
+ agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
+
+ MockRemote.assert_called_once()
+ assert agent._is_remote is True
+ assert agent._remote_agent is not None
+
+
+class TestMCPAgentRun:
+ """Tests for MCPAgent.run"""
+
+ def _mock_llm(self):
+ llm = MagicMock()
+ llm._llm_type = "test-provider"
+ llm._identifying_params = {"model": "test-model"}
+ llm.with_structured_output = MagicMock(return_value=llm)
+ return llm
+
+ @pytest.mark.asyncio
+ async def test_run_remote_delegates(self):
+ """In remote mode, run delegates to RemoteAgent.run and returns its result."""
+ with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
+ remote_instance = MockRemote.return_value
+ remote_instance.run = MagicMock()
+
+ async def _arun(*args, **kwargs):
+ return "remote-result"
+
+ remote_instance.run.side_effect = _arun
+
+ agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
+
+ result = await agent.run("hello", max_steps=3, external_history=["h"], output_schema=None)
+
+ remote_instance.run.assert_called_once()
+ assert result == "remote-result"
+
+ @pytest.mark.asyncio
+ async def test_run_local_calls_stream_and_consume(self):
+ """Local run creates stream generator and consumes it via _consume_and_return."""
+ llm = self._mock_llm()
+ client = MagicMock(spec=MCPClient)
+
+ agent = MCPAgent(llm=llm, client=client)
+
+ async def dummy_gen():
+ if False:
+ yield None
+
+ with (
+ patch.object(MCPAgent, "stream", return_value=dummy_gen()) as mock_stream,
+ patch.object(MCPAgent, "_consume_and_return") as mock_consume,
+ ):
+
+ async def _aconsume(gen):
+ return ("ok", 1)
+
+ mock_consume.side_effect = _aconsume
+
+ result = await agent.run("query", max_steps=2, manage_connector=True, external_history=None)
+
+ mock_stream.assert_called_once()
+ mock_consume.assert_called_once()
+ assert result == "ok"
+
+
+class TestMCPAgentStream:
+ """Tests for MCPAgent.stream"""
+
+ def _mock_llm(self):
+ llm = MagicMock()
+ llm._llm_type = "test-provider"
+ llm._identifying_params = {"model": "test-model"}
+ llm.with_structured_output = MagicMock(return_value=llm)
+ return llm
+
+ @pytest.mark.asyncio
+ async def test_stream_remote_delegates(self):
+ """In remote mode, stream delegates to RemoteAgent.stream and yields its items."""
+
+ async def _astream(*args, **kwargs):
+ yield "remote-yield-1"
+ yield "remote-yield-2"
+
+ with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
+ remote_instance = MockRemote.return_value
+ remote_instance.stream = MagicMock(side_effect=_astream)
+
+ agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
+
+ outputs = []
+ async for item in agent.stream("hello", max_steps=2):
+ outputs.append(item)
+
+ remote_instance.stream.assert_called_once()
+ assert outputs == ["remote-yield-1", "remote-yield-2"]
+
+ @pytest.mark.asyncio
+ async def test_stream_initializes_and_finishes(self):
+ """When not initialized, stream calls initialize, sets max_steps, and yields final output on AgentFinish."""
+ llm = self._mock_llm()
+ client = MagicMock(spec=MCPClient)
+ agent = MCPAgent(llm=llm, client=client)
+ agent.callbacks = []
+ agent.telemetry = MagicMock()
+
+ executor = MagicMock()
+ executor.max_iterations = None
+
+ async def _init_side_effect():
+ agent._agent_executor = executor
+ agent._initialized = True
+
+ with patch.object(MCPAgent, "initialize", side_effect=_init_side_effect) as mock_init:
+
+ async def _atake_next_step(**kwargs):
+ return AgentFinish(return_values={"output": "done"}, log="")
+
+ executor._atake_next_step = MagicMock(side_effect=_atake_next_step)
+
+ outputs = []
+ async for item in agent.stream("q", max_steps=3):
+ outputs.append(item)
+
+ mock_init.assert_called_once()
+ assert executor.max_iterations == 3
+ assert outputs[-1] == "done"
+ agent.telemetry.track_agent_execution.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_stream_uses_external_history_and_sets_max_steps(self):
+ """External history should be used, and executor.max_iterations should reflect max_steps arg."""
+ llm = self._mock_llm()
+ client = MagicMock(spec=MCPClient)
+ agent = MCPAgent(llm=llm, client=client)
+ agent.callbacks = []
+ agent.telemetry = MagicMock()
+
+ external_history = [HumanMessage(content="past")]
+
+ executor = MagicMock()
+ executor.max_iterations = None
+
+ async def _init_side_effect():
+ agent._agent_executor = executor
+ agent._initialized = True
+
+ with patch.object(MCPAgent, "initialize", side_effect=_init_side_effect):
+
+ async def _asserting_step(
+ name_to_tool_map=None, color_mapping=None, inputs=None, intermediate_steps=None, run_manager=None
+ ):
+ assert inputs["chat_history"] == [msg for msg in external_history]
+ return AgentFinish(return_values={"output": "ok"}, log="")
+
+ executor._atake_next_step = MagicMock(side_effect=_asserting_step)
+
+ outputs = []
+ async for item in agent.stream("query", max_steps=4, external_history=external_history):
+ outputs.append(item)
+
+ assert executor.max_iterations == 4
+ assert outputs[-1] == "ok"