Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/development/upgrade-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,50 @@ The environment variable for controlling the server banner has been renamed:

This change reflects that the setting now applies to all server startup methods, not just the CLI. The banner is now suppressed when running `python server.py` directly, not just when using `fastmcp run`.

### Context State Methods Are Async

<Warning>
**Breaking Change:** `ctx.set_state()` and `ctx.get_state()` are now async methods. Synchronous calls will fail.
</Warning>

Context state has changed from request-scoped to session-scoped, persisting across multiple tool calls within the same MCP session. The methods are now async because they interact with a pluggable storage backend.

<CodeGroup>
```python Before
@mcp.tool
def my_tool(ctx: Context) -> str:
ctx.set_state("key", "value")
value = ctx.get_state("key")
return value
```

```python After
@mcp.tool
async def my_tool(ctx: Context) -> str:
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
return value
```
</CodeGroup>

**What changed:**
- State now persists across requests within a session (not just within a single request)
- Different clients have isolated state (keyed by session ID)
- State expires after 1 day to prevent unbounded memory growth
- New method: `await ctx.delete_state(key)`

**Custom storage backends:**

By default, state uses an in-memory store. For distributed deployments, provide a custom backend:

```python
from key_value.aio.stores.redis import RedisStore

mcp = FastMCP("server", session_state_store=RedisStore(...))
```

See [Session State](/servers/context#session-state) for full documentation.
Comment on lines +259 to +301
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value-aio RedisStore documentation

💡 Result:

You can find the RedisStore docs and usage in the py-key-value-aio project repository (README + stores documentation). Key points:

  • Package / repo: py-key-value-aio — full docs, API reference, and store implementations (including RedisStore). [1]
  • Example usage: from key_value.aio.stores.redis import RedisStore; create with RedisStore(url="redis://localhost:6379/0") and use via the AsyncKeyValue protocol (put/get/delete/ttl). [1]
  • PyPI project page (package info / releases). [2]

Sources:
[1] strawgate/py-key-value (GitHub) — README + stores documentation.
[2] py-key-value-aio on PyPI.


🏁 Script executed:

# Search the codebase for RedisStore usage and context
rg -i "redisstore|key_value|session_state_store" --type python -A 3 -B 3

Repository: jlowin/fastmcp

Length of output: 144


🏁 Script executed:

# Search for any existing examples or documentation about state management
rg -i "ctx.set_state|ctx.get_state|ctx.delete_state" --type mdx -A 5 -B 2

Repository: jlowin/fastmcp

Length of output: 85


🏁 Script executed:

# Find the upgrade-guide.mdx file and read the relevant section
find . -name "upgrade-guide.mdx" -type f

Repository: jlowin/fastmcp

Length of output: 94


🏁 Script executed:

# Search for RedisStore in Python files
rg "RedisStore" -t py -B 2 -A 5

Repository: jlowin/fastmcp

Length of output: 695


🏁 Script executed:

# Search for session_state_store in the codebase
rg "session_state_store" -t py -B 3 -A 3

Repository: jlowin/fastmcp

Length of output: 1111


🏁 Script executed:

# Look for context state examples in any documentation
fd -e mdx -e md | head -20 | xargs rg "ctx.set_state|ctx.get_state" -B 2 -A 2 2>/dev/null || true

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

# Read the upgrade-guide.mdx file, focusing on lines 259-301
sed -n '259,301p' ./docs/development/upgrade-guide.mdx

Repository: jlowin/fastmcp

Length of output: 1397


🏁 Script executed:

# Also check for existing error handling patterns in other MDX docs
fd -e mdx -path "*/docs/*" | xargs grep -l "try\|except\|error\|Error" | head -5

Repository: jlowin/fastmcp

Length of output: 226


🏁 Script executed:

# Check for existing error handling patterns in code examples
grep -r "try:\|except\|ConnectionError\|TimeoutError" docs/development/*.mdx | head -10

Repository: jlowin/fastmcp

Length of output: 690


🏁 Script executed:

# Read the context around the session state documentation
sed -n '1,50p' ./docs/development/upgrade-guide.mdx | head -20

Repository: jlowin/fastmcp

Length of output: 725


🏁 Script executed:

# Check what the Session State documentation at /servers/context#session-state contains
find . -path "*/docs/servers/context*" -type f

Repository: jlowin/fastmcp

Length of output: 84


🏁 Script executed:

# Look for other code examples in upgrade-guide.mdx to see if they have error handling
grep -n "```python" docs/development/upgrade-guide.mdx | head -20

Repository: jlowin/fastmcp

Length of output: 144


🏁 Script executed:

# Check the full context of the FastMCP session_state_store parameter usage
sed -n '1,100p' src/fastmcp/server/server.py | grep -A 10 "session_state_store"

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

# Read the session state documentation
cat docs/servers/context.mdx | head -200

Repository: jlowin/fastmcp

Length of output: 7766


🏁 Script executed:

# Look at the RedisStore example in event_store.py more carefully
sed -n '1,50p' src/fastmcp/server/event_store.py

Repository: jlowin/fastmcp

Length of output: 1728


Include complete RedisStore configuration with URL parameter and error handling in the migration example.

The RedisStore example is incomplete—it shows RedisStore(...) without the required url parameter. Per MDX documentation guidelines, add realistic configuration (e.g., url="redis://localhost:6379"), necessary imports, connection error handling, and an expected outcome. Include the ctx.delete_state(key) usage shown in the changelog:

from key_value.aio.stores.redis import RedisStore
from fastmcp import FastMCP
from fastmcp.server.context import Context

try:
    redis_store = RedisStore(url="redis://localhost:6379/0")
    mcp = FastMCP("server", session_state_store=redis_store)
except ConnectionError as e:
    # Fallback or error handling
    print(f"Redis connection failed: {e}")

Also demonstrate ctx.delete_state(key) in context to complete the migration picture.


## v2.14.0

### OpenAPI Parser Promotion
Expand Down
47 changes: 47 additions & 0 deletions docs/development/v3-notes/v3-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,37 @@ Documentation: `docs/servers/providers/transforms.mdx`, `docs/servers/visibility

---

## Session-Scoped State

v3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session.

```python
@mcp.tool
async def increment_counter(ctx: Context) -> int:
count = await ctx.get_state("counter") or 0
await ctx.set_state("counter", count + 1)
return count + 1
```

State is automatically keyed by session ID, ensuring isolation between different clients. The implementation uses [pykeyvalue](https://github.com/strawgate/py-key-value) for pluggable storage backends:

```python
from key_value.aio.stores.redis import RedisStore

# Use Redis for distributed deployments
mcp = FastMCP("server", session_state_store=RedisStore(...))
```

**Key details:**
- Methods are now async: `await ctx.get_state()`, `await ctx.set_state()`, `await ctx.delete_state()`
- State expires after 1 day (TTL) to prevent unbounded memory growth
- Works during `on_initialize` middleware when using the same session object
- For distributed HTTP, session identity comes from the `mcp-session-id` header

Comment on lines +153 to +179
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value-aio RedisStore constructor parameters documentation

💡 Result:

RedisStore constructor (py-key-value-aio) — signatures and parameters

Signatures:

  • init(*, client: Redis, default_collection: str | None = None) -> None
  • init(*, url: str, default_collection: str | None = None) -> None
  • init(*, host: str = "localhost", port: int = 6379, db: int = 0, password: str | None = None, default_collection: str | None = None) -> None

Parameters:

  • client (Redis | None): existing redis-py client to use (store will not manage its lifecycle).
  • url (str | None): Redis URL (e.g. "redis://localhost:6379/0").
  • host (str): Redis host (default "localhost").
  • port (int): Redis port (default 6379).
  • db (int): Redis DB number (default 0).
  • password (str | None): Redis password (default None).
  • default_collection (str | None): default collection to use when none is provided (default None).

Source: py-key-value API docs (RedisStore init). [1]

[1] py-key-value API Reference — RedisStore init.


Expand the session-state examples to be complete and runnable with realistic RedisStore configuration.

The code snippets use RedisStore(...) as a placeholder and omit server imports and setup. Per documentation guidelines, provide a complete, executable example with concrete RedisStore parameters (e.g., host, port, db, or url), proper error handling with concrete exception types, and expected output for verification. The RedisStore constructor accepts: host/port/db parameters, a Redis url, or an existing client object.

Documentation: `docs/servers/context.mdx`

---

## Visibility System

Components can be dynamically enabled/disabled at runtime using the visibility system (`src/fastmcp/server/transforms/visibility.py`).
Expand Down Expand Up @@ -537,3 +568,19 @@ See `docs/development/v3-notes/auth-provider-env-vars.mdx` for rationale.
`FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER`

Now applies to all server startup methods, not just the CLI.

### Context State Methods Are Async

`ctx.set_state()` and `ctx.get_state()` are now async and session-scoped:

```python
# v2.x
ctx.set_state("key", "value")
value = ctx.get_state("key")

# v3.0
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
```
Comment on lines +572 to +584
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify or link to a full runnable example for the async state migration snippet.

This before/after block is helpful, but it isn’t runnable in isolation. Consider adding a short note that it’s illustrative and link to a complete example (with imports, setup, and error handling) for copy-paste use. As per coding guidelines, ...


State now persists across requests within a session. See "Session-Scoped State" above.
74 changes: 38 additions & 36 deletions docs/servers/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The `Context` object provides a clean interface to access MCP features within yo
- **Prompt Access**: List and retrieve prompts registered with the server
- **LLM Sampling**: Request the client's LLM to generate text based on provided messages
- **User Elicitation**: Request structured input from users during tool execution
- **State Management**: Store and share data between middleware and the handler within a single request
- **Session State**: Store data that persists across requests within an MCP session
- **Request Information**: Access metadata about the current request
- **Server Access**: When needed, access the underlying FastMCP server instance

Expand Down Expand Up @@ -209,55 +209,57 @@ messages = result.messages
- **`ctx.list_prompts() -> list[MCPPrompt]`**: Returns list of all available prompts
- **`ctx.get_prompt(name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult`**: Get a specific prompt with optional arguments

### State Management
### Session State

<VersionBadge version="2.11.0" />
<VersionBadge version="3.0.0" />

Store and share data between middleware and handlers within a single MCP request. Each MCP request (such as calling a tool, reading a resource, listing tools, or listing resources) receives its own context object with isolated state. Context state is particularly useful for passing information from [middleware](/servers/middleware) to your handlers.
Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.

To store a value in the context state, use `ctx.set_state(key, value)`. To retrieve a value, use `ctx.get_state(key)`.
```python
from fastmcp import FastMCP, Context

<Warning>
Context state is scoped to a single MCP request. Each operation (tool call, resource read, list operation, etc.) receives a new context object. State set during one request will not be available in subsequent requests. For persistent data storage across requests, use external storage mechanisms like databases, files, or in-memory caches.
</Warning>
mcp = FastMCP("stateful-app")

This simplified example shows how to use MCP middleware to store user info in the context state, and how to access that state in a tool:
@mcp.tool
async def increment_counter(ctx: Context) -> int:
"""Increment a counter that persists across tool calls."""
count = await ctx.get_state("counter") or 0
await ctx.set_state("counter", count + 1)
return count + 1

```python {7-8, 16-17}
from fastmcp.server.middleware import Middleware, MiddlewareContext
@mcp.tool
async def get_counter(ctx: Context) -> int:
"""Get the current counter value."""
return await ctx.get_state("counter") or 0
```

class UserAuthMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
Each client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.

# Middleware stores user info in context state
context.fastmcp_context.set_state("user_id", "user_123")
context.fastmcp_context.set_state("permissions", ["read", "write"])
**Method signatures:**
- **`await ctx.set_state(key: str, value: Any) -> None`**: Store a value in session state
- **`await ctx.get_state(key: str) -> Any`**: Retrieve a value (returns None if not found)
- **`await ctx.delete_state(key: str) -> None`**: Remove a value from session state

return await call_next(context)
<Note>
State methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.
</Note>
Comment on lines +212 to +245
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the Session State section comply with doc standards (runnable + error handling + expected output).

The new example doesn’t show error handling or expected results and the prose isn’t consistently second person. Please make it a runnable example (or clearly indicate how to run it) with error handling and expected outcomes. As per coding guidelines, update this section to meet the MDX documentation requirements.

✅ Proposed fix (partial)
-Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+You can store data that persists across multiple requests within the same MCP session. FastMCP automatically keys session state by the client's session to isolate clients.

 ```python
+ # examples/session_state.py
 from fastmcp import FastMCP, Context
 
 mcp = FastMCP("stateful-app")
 
 `@mcp.tool`
 async def increment_counter(ctx: Context) -> int:
     """Increment a counter that persists across tool calls."""
-    count = await ctx.get_state("counter") or 0
-    await ctx.set_state("counter", count + 1)
-    return count + 1
+    try:
+        count = await ctx.get_state("counter") or 0
+        await ctx.set_state("counter", count + 1)
+        return count + 1
+    except Exception as exc:
+        await ctx.error(f"Failed to update counter: {exc}")
+        raise
 
 `@mcp.tool`
 async def get_counter(ctx: Context) -> int:
     """Get the current counter value."""
     return await ctx.get_state("counter") or 0

+Expected results:
+- First call to increment_counter returns 1.
+- Second call to increment_counter returns 2.

</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->


@mcp.tool
async def secure_operation(data: str, ctx: Context) -> str:
"""Tool can access state set by middleware."""
#### Custom Storage Backends

user_id = ctx.get_state("user_id") # "user_123"
permissions = ctx.get_state("permissions") # ["read", "write"]
if "write" not in permissions:
return "Access denied"
return f"Processing {data} for user {user_id}"
By default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:

```python
from key_value.aio.stores.redis import RedisStore

# Use Redis for distributed state
mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
```
Comment on lines +247 to 256
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value RedisStore initialization documentation

💡 Result:

Short answer — py-key-value's RedisStore (async library: py-key-value-aio) accepts either a Redis client or connection info (URL / host / port / password / client kwargs) when you initialize it.

Example usages:

  • With redis URL:
    from key_value.aio.stores.redis import RedisStore
    store = RedisStore(url="redis://localhost:6379/0"). (github.com)

  • With host/port (common in docs / FastMCP examples):
    from key_value.aio.stores.redis import RedisStore
    store = RedisStore(host="redis.example.com", port=6379, password="secret"). (fastmcp.mintlify.app)

Key parameters (per the library docs):

  • client: an existing Redis client/connection (optional).
  • redis_url / url: Redis connection URL (optional).
  • client_kwargs: dict of kwargs passed to the Redis client.
  • ttl: default TTL (seconds) applied to keys (optional).
  • namespace: optional key prefix/namespace. (api.python.langchain.com)

If you want, I can open the specific RedisStore source file and show the exact init signature and supported kwargs.

Citations:


🌐 Web query:

key_value.aio.stores.redis RedisStore constructor parameters

💡 Result:

Constructor signature (keyword-only) and parameters for RedisStore:

  • client: Any | None — a Redis client/connection instance. [1]
  • redis_url: str | None — Redis connection URL (alternative to client). [1]
  • client_kwargs: dict | None — kwargs passed to the Redis client. [1]
  • ttl: int | None — default TTL in seconds for stored keys (None = no expiry). [1]
  • namespace: str | None — optional prefix applied to all keys. [1][2]

Sources: LangChain RedisStore API/docs. [1] (api.python.langchain.com)

Citations:


🏁 Script executed:

# Check if there are any existing RedisStore usages or tests in the repository
rg -i "redisstore" --type py -A 3 -B 1

Repository: jlowin/fastmcp

Length of output: 505


Replace the RedisStore placeholder with a runnable, verified example.

The current code at line 255 uses RedisStore(...) without a real connection configuration, error handling, or expected outcomes. Per MDX documentation guidelines, include a complete, runnable example with proper prerequisites and error handling.

Based on the py-key-value library API, RedisStore accepts a url parameter (e.g., redis://localhost:6379) or host/port parameters. Include:

  • A real connection string or configuration parameters
  • Prerequisites (Redis server must be running)
  • Error handling for connection failures
  • Expected behavior after successful initialization

Example pattern (reference: src/fastmcp/server/event_store.py):

from key_value.aio.stores.redis import RedisStore

# Use Redis for distributed state
redis_backend = RedisStore(url="redis://localhost:6379")
mcp = FastMCP("distributed-app", session_state_store=redis_backend)

Also document prerequisites, connection error handling, and any configuration needed (e.g., Redis server address, authentication).


**Method signatures:**
- **`ctx.set_state(key: str, value: Any) -> None`**: Store a value in the context state
- **`ctx.get_state(key: str) -> Any`**: Retrieve a value from the context state (returns None if not found)
Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/py-key-value) `AsyncKeyValue` protocol works. See [Storage Backends](/servers/storage-backends) for more options including Redis, DynamoDB, and MongoDB.

**State Inheritance:**
When a new context is created (nested contexts), it inherits a copy of its parent's state. This ensures that:
- State set on a child context never affects the parent context
- State set on a parent context after the child context is initialized is not propagated to the child context
#### State During Initialization

This makes state management predictable and prevents unexpected side effects between nested operations.
State set during `on_initialize` middleware persists to subsequent tool calls when using the same session object (STDIO, SSE, single-server HTTP). For distributed/serverless HTTP deployments where different machines handle init and tool calls, state is isolated by the `mcp-session-id` header.

### Change Notifications

Expand Down Expand Up @@ -345,7 +347,7 @@ async def request_info(ctx: Context) -> dict:

- **`ctx.request_id -> str`**: Get the unique ID for the current MCP request
- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization
- **`ctx.session_id -> str | None`**: Get the MCP session ID for session-based data sharing (HTTP transports only)
- **`ctx.session_id -> str`**: Get the MCP session ID for session-based data sharing. Raises `RuntimeError` if the MCP session is not yet established.

#### Request Context Availability

Expand Down
51 changes: 51 additions & 0 deletions examples/persistent_state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Persistent Session State

This example demonstrates session-scoped state that persists across tool calls within the same MCP session.

## What it shows

- State set in one tool call is readable in subsequent calls
- Different clients have isolated state (same keys, different values)
- Reconnecting creates a new session with fresh state

## Running

**HTTP transport:**

```bash
# Terminal 1: Start the server
uv run python server.py

# Terminal 2: Run the client
uv run python client.py
```

**STDIO transport (in-process):**

```bash
uv run python client_stdio.py
```

## Example output

```text
Each line below is a separate tool call

Alice connects
session a9f6eaa3
set user = Alice
set secret = alice-password
get user → Alice
get secret → alice-password

Bob connects (different session)
session 0c3bffc5
get user → not found
get secret → not found
set user = Bob
get user → Bob

Alice reconnects (new session)
session e39640e3
get user → not found
```
85 changes: 85 additions & 0 deletions examples/persistent_state/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Client for testing persistent state.

Run the server first:
uv run python examples/persistent_state/server.py

Then run this client:
uv run python examples/persistent_state/client.py
"""

import asyncio

from rich.console import Console

from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport

URL = "http://127.0.0.1:8000/mcp"
console = Console()
Comment on lines +17 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file
find . -type f -name "client.py" | grep -E "persistent_state|examples" | head -20

Repository: jlowin/fastmcp

Length of output: 466


🏁 Script executed:

# Check if the file exists and read its content
if [ -f "examples/persistent_state/client.py" ]; then
  echo "=== File exists. Reading content with line numbers ===";
  cat -n examples/persistent_state/client.py | head -30
else
  echo "File not found at examples/persistent_state/client.py"
  find . -type f -name "*.py" | grep -i persistent | head -10
fi

Repository: jlowin/fastmcp

Length of output: 1052


🏁 Script executed:

# Check Python version requirement in the project
find . -maxdepth 2 -type f \( -name "pyproject.toml" -o -name "setup.py" -o -name "setup.cfg" -o -name "requirements.txt" \) | xargs grep -l "python" 2>/dev/null || echo "No version markers found in common locations"

Repository: jlowin/fastmcp

Length of output: 144


Add required type annotations for module globals.

Lines 17-18 are missing type annotations for URL and console, which violates the repo's Python typing policy.

✅ Proposed fix
-URL = "http://127.0.0.1:8000/mcp"
-console = Console()
+URL: str = "http://127.0.0.1:8000/mcp"
+console: Console = Console()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
URL = "http://127.0.0.1:8000/mcp"
console = Console()
URL: str = "http://127.0.0.1:8000/mcp"
console: Console = Console()



async def main() -> None:
console.print()
console.print("[dim italic]Each line below is a separate tool call[/dim italic]")
console.print()

# --- Alice's session ---
console.print("[dim]Alice connects[/dim]")

transport1 = StreamableHttpTransport(url=URL)
async with Client(transport=transport1) as alice:
result = await alice.call_tool("list_session_info", {})
console.print(f" session [cyan]{result.data['session_id'][:8]}[/cyan]")

await alice.call_tool("set_value", {"key": "user", "value": "Alice"})
console.print(" set [white]user[/white] = [green]Alice[/green]")

await alice.call_tool("set_value", {"key": "secret", "value": "alice-password"})
console.print(" set [white]secret[/white] = [green]alice-password[/green]")

result = await alice.call_tool("get_value", {"key": "user"})
console.print(" get [white]user[/white] → [green]Alice[/green]")

result = await alice.call_tool("get_value", {"key": "secret"})
console.print(" get [white]secret[/white] → [green]alice-password[/green]")

console.print()

# --- Bob's session ---
console.print("[dim]Bob connects (different session)[/dim]")

transport2 = StreamableHttpTransport(url=URL)
async with Client(transport=transport2) as bob:
result = await bob.call_tool("list_session_info", {})
console.print(f" session [cyan]{result.data['session_id'][:8]}[/cyan]")

await bob.call_tool("get_value", {"key": "user"})
console.print(" get [white]user[/white] → [dim]not found[/dim]")

await bob.call_tool("get_value", {"key": "secret"})
console.print(" get [white]secret[/white] → [dim]not found[/dim]")

await bob.call_tool("set_value", {"key": "user", "value": "Bob"})
console.print(" set [white]user[/white] = [green]Bob[/green]")

await bob.call_tool("get_value", {"key": "user"})
console.print(" get [white]user[/white] → [green]Bob[/green]")

console.print()

# --- Alice reconnects ---
console.print("[dim]Alice reconnects (new session)[/dim]")

transport3 = StreamableHttpTransport(url=URL)
async with Client(transport=transport3) as alice_again:
result = await alice_again.call_tool("list_session_info", {})
console.print(f" session [cyan]{result.data['session_id'][:8]}[/cyan]")

await alice_again.call_tool("get_value", {"key": "user"})
console.print(" get [white]user[/white] → [dim]not found[/dim]")

console.print()


if __name__ == "__main__":
asyncio.run(main())
Loading