Stateful vs Stateless MCP Clients in AgentScope: Architecture and Usage Guide
Stateful MCP clients maintain a persistent ClientSession requiring explicit connect() and close() calls, while stateless MCP clients create temporary sessions per request with automatic cleanup, both exposing identical APIs through list_tools() and get_callable_function().
AgentScope, the open-source multi-agent framework from agentscope-ai/agentscope, implements the Model Context Protocol (MCP) through two distinct client architectures. Understanding the difference between stateful and stateless MCP clients in AgentScope is essential for optimizing connection overhead, managing authentication state, and selecting the appropriate transport mechanism for your agent tools.
What Are Stateful and Stateless MCP Clients?
Stateful MCP clients in AgentScope are designed for long-running interactions that require session continuity. These clients establish a persistent connection to the MCP server, maintaining state such as authentication cookies, browsing history, or conversation context across multiple tool invocations. According to the source code in src/agentscope/mcp/_stateful_client_base.py, these clients store a ClientSession object in self.session and manage its lifecycle through an AsyncExitStack.
Stateless MCP clients provide a lightweight alternative for ephemeral interactions. These clients create a fresh ClientSession for every individual tool call, automatically disposing of the transport and session immediately after the RPC completes. As implemented in src/agentscope/mcp/_http_stateless_client.py, the stateless variant uses a disposable context manager pattern via get_client() rather than maintaining persistent socket connections.
Key Differences Between Stateful and Stateless MCP Clients
Session Lifetime and Resource Management
The fundamental distinction lies in how each client manages the underlying MCP session and transport resources.
Stateful clients maintain open sockets or subprocesses for the entire lifetime of the client object. In src/agentscope/mcp/_stateful_client_base.py (lines 46-70), the connect() method initializes an AsyncExitStack, enters the transport context (either sse_client or streamablehttp_client), creates a ClientSession, and calls session.initialize(). This session remains active until close() is explicitly invoked, holding file descriptors and memory throughout.
Stateless clients minimize resource usage by releasing connections immediately after each call. The HttpStatelessClient class in src/agentscope/mcp/_http_stateless_client.py (lines 79-89) implements get_client() to return a temporary async context manager. Each invocation of list_tools() or get_callable_function() enters this context, creates a transient ClientSession, executes the RPC, and exits to automatically close the transport.
Connection Lifecycle Requirements
Stateful clients require explicit lifecycle management. You must call await client.connect() before any tool operations and await client.close() when finished. The StatefulClientBase class tracks connection state via self.is_connected and raises errors if methods are called while disconnected. This pattern is mandatory for both HttpStatefulClient and StdIOStatefulClient variants.
Stateless clients require no explicit connection management. The client acts as a lightweight wrapper that internally handles context creation and cleanup. As shown in src/agentscope/mcp/_http_stateless_client.py (lines 46-53), the list_tools method simply enters the disposable client context, executes the MCP list_tools call, and returns results without exposing session management to the user.
Performance and Error Handling Characteristics
Stateful clients incur lower per-call overhead after the initial connection but maintain persistent resource consumption. They are susceptible to session-level errors; if the underlying transport fails, all subsequent tool calls fail until connect() is called again to establish a fresh session.
Stateless clients trade connection overhead for isolation. Each call bears the cost of transport initialization and ClientSession creation, making them less efficient for high-frequency sequential calls. However, errors are isolated to individual calls, and memory/connection leaks are impossible since resources are immediately released.
Implementation Details in the AgentScope Source Code
The architectural distinction is enforced through inheritance patterns in the src/agentscope/mcp/ directory.
Stateful Implementation: The StatefulClientBase class in _stateful_client_base.py inherits from MCPClientBase and adds session persistence. It stores self.session, self.stack (an AsyncExitStack), and self.is_connected. The concrete implementations HttpStatefulClient (_http_stateful_client.py) and StdIOStatefulClient (_stdio_stateful_client.py) build their respective transports (HTTP with SSE or Streamable-HTTP, or subprocess StdIO) within this persistent framework.
Stateless Implementation: The HttpStatelessClient in _http_stateless_client.py inherits directly from MCPClientBase and implements the get_client() abstract method to return disposable context managers. It uses sse_client or streamablehttp_client transports but never stores them as instance variables, ensuring each RPC operates in isolation.
Code Examples: Using Stateful and Stateless MCP Clients
Stateless HTTP Client Example
The stateless HTTP client requires no connection management. Each method call creates and destroys a temporary session automatically.
from agentscope.mcp import HttpStatelessClient
stateless = HttpStatelessClient(
name="maps_stateless",
transport="streamable_http",
url="https://mcp.example.com/mcp",
)
# Each call creates a fresh session under the hood
tools = await stateless.list_tools()
geo_fn = await stateless.get_callable_function(
func_name="maps_geo",
wrap_tool_result=True,
)
result = await geo_fn(address="Tiananmen Square", city="Beijing")
print(result)
Stateful HTTP Client Example
The stateful HTTP client requires explicit lifecycle management via connect() and close() to maintain a persistent session.
from agentscope.mcp import HttpStatefulClient
stateful = HttpStatefulClient(
name="maps_stateful",
transport="streamable_http",
url="https://mcp.example.com/mcp",
)
await stateful.connect() # establish persistent session
tools = await stateful.list_tools()
geo_fn = await stateful.get_callable_function(
func_name="maps_geo",
wrap_tool_result=True,
)
result = await geo_fn(address="Tiananmen Square", city="Beijing")
print(result)
await stateful.close() # clean up sockets
StdIO Stateful Client Example
AgentScope only provides a stateful variant for StdIO transport, as subprocess-based servers typically require persistent process management.
from agentscope.mcp import StdIOStatefulClient
stdio = StdIOStatefulClient(
name="my_stdio",
command="python -m my_mcp_server",
)
await stdio.connect()
tools = await stdio.list_tools()
# ... use tools ...
await stdio.close()
When to Choose Stateful vs Stateless MCP Clients
Select stateful MCP clients when your application requires:
- Session continuity: Authentication cookies, browsing history, or conversation state must persist across multiple tool calls
- High-frequency sequential calls: Amortizing connection overhead across many operations
- Subprocess-based servers: StdIO transport inherently requires process lifecycle management
Select stateless MCP clients when your application requires:
- Minimal resource footprint: Short-lived connections that immediately release file descriptors and memory
- Fault isolation: Ensuring transport errors affect only single calls without crashing persistent sessions
- Simple integration: Eliminating boilerplate
connect()andclose()calls in ephemeral scripts or serverless functions
Summary
- Stateful MCP clients maintain a persistent
ClientSessionthroughout the client object's lifetime, requiring explicitconnect()andclose()calls to manage transport resources. - Stateless MCP clients create disposable sessions for each RPC call, automatically handling cleanup without manual lifecycle management.
- Both client types expose identical high-level APIs including
list_tools()andget_callable_function(), allowing seamless interchangeability based on resource and state requirements. - The architectural distinction is implemented in
src/agentscope/mcp/_stateful_client_base.pyversussrc/agentscope/mcp/_http_stateless_client.py.
Frequently Asked Questions
Can I convert a stateful MCP client to stateless behavior by calling close() after each tool use?
While technically possible, this approach defeats the purpose of stateful clients and incurs unnecessary overhead. The close() method in StatefulClientBase cleans up the AsyncExitStack and underlying transport, requiring a full reconnection via connect() for subsequent calls. For per-call session isolation, use HttpStatelessClient instead, which is optimized for this pattern in src/agentscope/mcp/_http_stateless_client.py.
Why does AgentScope only provide stateful clients for StdIO transport?
StdIO-based MCP servers run as subprocesses that require explicit process lifecycle management. A stateless model would spawn and terminate subprocesses for every tool call, creating unacceptable overhead and potential resource leaks. The StdIOStatefulClient in src/agentscope/mcp/_stdio_stateful_client.py maintains the subprocess throughout the client lifetime, ensuring efficient communication via standard input/output streams.
Do stateful and stateless MCP clients support the same transport protocols?
HTTP-based clients support both Server-Sent Events (SSE) and Streamable HTTP transports regardless of state management model. The HttpStatefulClient and HttpStatelessClient classes both accept transport="sse" or transport="streamable_http" parameters. However, StdIO transport is only available for stateful clients via StdIOStatefulClient, as stateless subprocess management is not implemented in the AgentScope codebase.
How does error handling differ between stateful and stateless MCP clients?
In stateful clients, transport or session errors persist until the client is explicitly reconnected. If the underlying ClientSession fails in src/agentscope/mcp/_stateful_client_base.py, all subsequent tool calls will raise connection errors until close() followed by connect() resets the state. In stateless clients, errors are isolated to individual calls because each RPC uses a fresh transport context created in src/agentscope/mcp/_http_stateless_client.py, preventing cascading failures across tool invocations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →