> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Lifecycle Management

> Context manager and cleanup for MCP connections

MCP connections in PraisonAI Agents support context managers and explicit `shutdown()` so subprocesses, streams, and sockets close reliably.

> `MCP(...)` accepts three equivalent construction forms — see [Three equivalent forms](/docs/features/mcp#three-equivalent-forms).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, MCP

with MCP("uvx mcp-server-time") as mcp:
    agent = Agent(name="TimeAgent", instructions="Report the current time.", tools=mcp)
    agent.start("What time is it in UTC?")
```

The user runs an agent inside an MCP context manager; connections shut down cleanly when the session ends.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[🤖 Agent] --> M[MCP]
    M --> CM[with MCP ...]
    CM --> SD[shutdown]
    SD --> X[✅ Clean exit]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class M,CM process
    class SD,X result
```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, MCP

    with MCP("uvx mcp-server-time") as mcp:
        agent = Agent(
            name="TimeAgent",
            instructions="Get the current time",
            tools=mcp
        )
        print(agent.start("What time is it?"))
    # Connection closed automatically
    ```
  </Step>

  <Step title="With Configuration">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import os
    from praisonaiagents import Agent, MCP

    with MCP(
        command="npx",
        args=["-y", "@modelcontextprotocol/server-brave-search"],
        env={"BRAVE_API_KEY": os.getenv("BRAVE_API_KEY")},
        timeout=30
    ) as mcp:
        agent = Agent(name="SearchAgent", tools=mcp)
        agent.start("Search for Python tutorials")
    ```
  </Step>
</Steps>

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant MCP as MCP Connection

    User->>MCP: with MCP(...) as mcp
    MCP->>MCP: Start subprocess / open stream
    User->>Agent: agent.start(request)
    Agent->>MCP: Call tools
    MCP-->>Agent: Tool results
    Agent-->>User: Response
    MCP->>MCP: __exit__ calls shutdown()
```

| Phase | What happens |
| - | - |
| 1. Enter | The `with` block opens the MCP connection |
| 2. Use | The agent calls MCP-provided tools |
| 3. Exit | `shutdown()` closes subprocesses, streams, and sockets |

## Agent-Managed Cleanup

When you pass an MCP client through the constructor (`tools=[MCP(...)]`), `agent.close()` and `agent.aclose()` now walk the agent's tools and shut down anything exposing `.shutdown()` / `.aclose()` — so the MCP subprocess and its background thread are cleaned up with the agent.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.mcp import MCP

agent = Agent(
    name="Coder",
    tools=[MCP("uvx some-mcp-server")],
    instructions="Use MCP tools",
)
try:
    agent.start("do work")
finally:
    agent.close()   # now correctly shuts down MCP subprocesses/threads
                    # (async: `await agent.aclose()`)
```

<Note>
  Constructor-pattern MCP clients (`tools=[MCP(...)]`) are now auto-shut-down by `agent.close()` / `agent.aclose()` — you don't need `remove_mcp_server()` any more just for cleanup. `aclose()` prefers a tool's `aclose()` and falls back to `shutdown()`.
</Note>

## Manual Cleanup

For cases where a context manager is not suitable:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import MCP

mcp = MCP("uvx mcp-server-time")

try:
    tools = mcp.get_tools()
finally:
    mcp.shutdown()
```

## Request Cancellation

An MCP client can abort an in-flight request by sending a `notifications/cancelled` notification naming the `requestId` it wants to stop.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Client
    participant Server

    Client->>Server: tools/call (id=42)
    Client->>Server: notifications/cancelled (requestId=42)
    Server->>Server: task.cancel()
    Server-->>Client: -32603 "Request cancelled"

    Note over Client,Server: A cancel that arrives before the task<br/>is registered is honoured immediately after
```

<Steps>
  <Step title="Send the request">
    The client calls a tool with a JSON-RPC id it can reference later.

    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    { "jsonrpc": "2.0", "id": 42, "method": "tools/call", "params": { "name": "search" } }
    ```
  </Step>

  <Step title="Cancel it">
    The client sends a cancellation notification with the same id as `requestId`.

    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    { "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 42 } }
    ```
  </Step>

  <Step title="Receive the cancelled response">
    The server cancels the running task and replies with a JSON-RPC error.

    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    { "jsonrpc": "2.0", "id": 42, "error": { "code": -32603, "message": "Request cancelled" } }
    ```
  </Step>
</Steps>

| Behaviour | What happens |
| - | - |
| Response | Cancelled id-bearing requests return `-32603` `"Request cancelled"`; notifications receive nothing |
| Ordering | A cancel that arrives before the task is registered is stashed and applied immediately after registration |
| Overflow | Up to 10,000 pending cancel ids are kept in FIFO order — the oldest are evicted first, so a burst never drops the newest cancel |

<Note>
  Only id-bearing requests can be cancelled. Fire-and-forget notifications have no id to reference and return nothing.
</Note>

## Lifecycle Methods

### Authenticating the HTTP transport

When `api_key` is configured on the MCP HTTP-stream server, **all** of GET, POST, and DELETE require:

```
Authorization: Bearer <key>
```

Comparison uses constant-time `hmac.compare_digest` (timing-attack resistant). Missing or wrong tokens return `401 Unauthorized` with `{"error": "Unauthorized"}`. DELETE returning 401 instead of 405 prevents information disclosure about whether sessions exist.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
curl -H "Authorization: Bearer ${MCP_API_KEY:?Set MCP_API_KEY}" https://localhost:8080/mcp
```

### `__enter__` / `__exit__`

Context manager protocol for automatic resource management:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
with MCP(command) as mcp:
    pass
# __exit__ calls shutdown() automatically
```

### `shutdown()`

Explicitly close all connections and cleanup resources:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
mcp = MCP("uvx mcp-server-time")
mcp.shutdown()
```

`shutdown()` closes an MCP connection. For stdio transports it enqueues a sentinel on the runner queue, **joins the background thread**, and lets the transport terminate its child process. For HTTP-stream / WebSocket transports it dispatches to `close()` (or `aclose()` if awaitable). It is idempotent — a second call is a no-op — and it never raises: cleanup errors are swallowed as best-effort.

<Note>
  `shutdown()` now joins the runner thread and terminates the stdio subprocess in the same call. In long-lived processes (gateways, API servers, notebooks, CI parallel tests) this prevents one leaked `npx` / `node` child per agent lifecycle — a regression fixed in [PR #5078](https://github.com/MervinPraison/PraisonAI/pull/5078).
</Note>

<Note>
  `shutdown()` also **releases the server-name registry entries** this instance registered via `with_tool_prefix()` — one decrement per registered name, deduping the raw/sanitised pair. The release is guarded by an internal `_server_names_released` flag so a second `shutdown()` or `__del__` never underflows another live holder's count. The consequence: `MCP.list_active_server_names()` reflects live instances, which is exactly what the [STRICT skill capability gate](/docs/features/skill-capability-gates) reads. Added in [PR #5136](https://github.com/MervinPraison/PraisonAI/pull/5136).
</Note>

| Transport | Client method invoked | Cleanup performed |
| - | - | - |
| Stdio | subprocess terminate + stream close | Sentinel → thread join → subprocess terminate |
| SSE | HTTP stream close | Stream close |
| HTTP Stream | `close()` (fallback) | Session close |
| WebSocket | `close()` (fallback) | Socket close |

### Idempotency & retry

`shutdown()` is safe to call multiple times:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import MCP

mcp = MCP("uvx mcp-server-time")
mcp.shutdown()
mcp.shutdown()   # no-op — self.runner is None
```

If an in-flight tool call keeps the runner thread alive past the join timeout, the runner reference is kept so a later `shutdown()` can retry the join. The thread is a daemon, so a surviving runner never blocks interpreter exit.

<Note>
  `MCPToolRunner.stop()` is the internal helper that queues the sentinel **and** joins the daemon thread. Since [PR #5078](https://github.com/MervinPraison/PraisonAI/pull/5078) it runs on both normal `MCP.shutdown()` (via the `MCP.shutdown()` → `runner.stop()` call) and on construction failures (handshake timeout / init error). It is not a public API — public shutdown stays `mcp.shutdown()` or the context-manager form. See [PR #5078](https://github.com/MervinPraison/PraisonAI/pull/5078) and [issue #5074](https://github.com/MervinPraison/PraisonAI/issues/5074).
</Note>

### `__del__`

Destructor ensures cleanup even if `shutdown()` was not called:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
mcp = MCP("uvx mcp-server-time")
del mcp  # __del__ calls shutdown()
```

## Connection Types

MCP supports multiple connection types, all with proper cleanup:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Stdio
with MCP("uvx mcp-server-time") as mcp: ...

# SSE
with MCP("http://localhost:8080/sse") as mcp: ...

# HTTP Stream — pass the full endpoint URL your server exposes (e.g. /mcp)
with MCP("http://localhost:8080/mcp") as mcp: ...

# WebSocket
with MCP("ws://localhost:8080") as mcp: ...
```

All four transports now close uniformly on `shutdown()`. HTTP-stream and WebSocket close via the `close()` fallback inside `MCP.shutdown()` — previously these two leaked subprocesses/threads on shutdown, while stdio and SSE cleaned up correctly.

## Long-running processes

Gateways, HTTP invoke APIs, Jupyter kernels, and CI test runners create many `Agent(mcp=...)` instances over their lifetime. Every stdio MCP spawns a child process (`npx` / `node`) and a daemon runner thread. If those aren't torn down when the agent is done, the host process leaks one child per MCP per agent.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Server as Long-lived Process
    participant Agent
    participant MCP
    participant Child as npx / node

    loop Each request
        Server->>Agent: Agent(tools=[MCP(...)])
        Agent->>MCP: __init__
        MCP->>Child: spawn stdio subprocess
        Server->>Agent: agent.start(...)
        Server->>Agent: agent.close()
        Agent->>MCP: shutdown()
        MCP->>Child: terminate (thread join + stream close)
    end

    Note over Server,Child: With PR #5078, each iteration ends<br/>with zero leaked children.
```

The recommended pattern for long-lived hosts is a `try/finally` around every agent that carries MCP tools:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, MCP

def handle_request(prompt: str) -> str:
    agent = Agent(
        name="Assistant",
        tools=[MCP("uvx mcp-server-time")],
    )
    try:
        return agent.start(prompt)
    finally:
        agent.close()   # walks tools, calls MCP.shutdown() on each
```

Async equivalent:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent, MCP

async def handle_request(prompt: str) -> str:
    async with Agent(
        name="Assistant",
        tools=[MCP("uvx mcp-server-time")],
    ) as agent:
        return await agent.achat(prompt)
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always use a context manager">
    Prefer `with MCP(...) as mcp:` — cleanup runs even when an exception is raised.
  </Accordion>

  <Accordion title="Handle exceptions inside the block">
    Wrap tool calls in `try/except` inside the `with` block; `__exit__` still closes the connection.
  </Accordion>

  <Accordion title="Nest multiple MCP instances carefully">
    Open each MCP in its own `with` block or nest them — both instances shut down in reverse order.
  </Accordion>

  <Accordion title="Pass secrets via env, not inline">
    Use the `env=` parameter with `os.getenv(...)` rather than hard-coding API keys in recipe files.
  </Accordion>

  <Accordion title="Close every agent in long-running hosts">
    Gateways, API servers, and notebooks accumulate leaked `node` / `npx` children if `MCP.shutdown()` (or `agent.close()`) is never called. Wrap each request or session in `try/finally` and call `agent.close()` — or use `with MCP(...) as mcp:` for per-MCP scope. Since [PR #5078](https://github.com/MervinPraison/PraisonAI/pull/5078), `shutdown()` reliably terminates the stdio subprocess and joins the runner thread.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="MCP CLI" icon="terminal" href="/docs/cli/mcp">
    Run and inspect MCP servers from the terminal
  </Card>

  <Card title="MCP Transports" icon="plug" href="/docs/mcp/transports">
    Stdio, SSE, HTTP stream, and WebSocket options
  </Card>

  <Card title="Agent Lifecycle Cleanup" icon="broom" href="/docs/features/agent-lifecycle-cleanup">
    How agent.close() shuts down MCP tools and breakers
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.