Skip to content
Merged
7 changes: 7 additions & 0 deletions docs/servers/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The `Context` object provides a clean interface to access MCP features within yo
- **LLM Sampling**: Request the client's LLM to generate text based on provided messages
- **User Elicitation**: Request structured input from users during tool execution
- **Session State**: Store data that persists across requests within an MCP session
- **Session Visibility**: [Control which components are visible](/servers/enabled#per-session-visibility) to the current session
- **Request Information**: Access metadata about the current request
- **Server Access**: When needed, access the underlying FastMCP server instance

Expand Down Expand Up @@ -261,6 +262,12 @@ Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/

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.

### Session Visibility

<VersionBadge version="3.0.0" />

Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_components()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/enabled#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation.

### Change Notifications

<VersionBadge version="3.0.0" />
Expand Down
151 changes: 148 additions & 3 deletions docs/servers/enabled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Component Visibility
sidebarTitle: Component Visibility
description: Control which components are available to clients
icon: toggle-on
tag: NEW
---

import { VersionBadge } from '/snippets/version-badge.mdx'
Expand Down Expand Up @@ -285,9 +286,153 @@ def check_permissions(ctx: Context) -> str:
return "Admin tools disabled"
```

<Warning>
Dynamic enabled state changes affect all connected clients. For per-user filtering, consider using separate server instances or implementing authorization in the tools themselves.
</Warning>
## Per-Session Visibility

Server-level visibility changes affect all connected clients simultaneously. When you need different clients to see different components, use per-session visibility instead.

Session visibility lets individual sessions customize their view of available components. When a tool calls `ctx.enable_components()` or `ctx.disable_components()`, those rules apply only to the current session. Other sessions continue to see the global defaults. This enables patterns like progressive disclosure, role-based access, and on-demand feature activation.

```python
from fastmcp import FastMCP
from fastmcp.server.context import Context

mcp = FastMCP("Session-Aware Server")

@mcp.tool(tags={"premium"})
def premium_analysis(data: str) -> str:
"""Advanced analysis available to premium users."""
return f"Premium analysis of: {data}"

@mcp.tool
async def unlock_premium(ctx: Context) -> str:
"""Unlock premium features for this session."""
await ctx.enable_components(tags={"premium"})
return "Premium features unlocked"

@mcp.tool
async def reset_features(ctx: Context) -> str:
"""Reset to default feature set."""
await ctx.reset_components()
return "Features reset to defaults"

# Premium tools are disabled globally by default
mcp.disable(tags={"premium"})
```

All sessions start with `premium_analysis` hidden. When a session calls `unlock_premium`, that session gains access to premium tools while other sessions remain unaffected. Calling `reset_features` returns the session to the global defaults.

Comment thread
jlowin marked this conversation as resolved.
### How Session Rules Work

Session rules override global transforms. When listing components, FastMCP first applies global enable/disable rules, then applies session-specific rules on top. Rules within a session accumulate, and later rules override earlier ones for the same component.

```python
@mcp.tool
async def customize_session(ctx: Context) -> str:
# Enable finance tools for this session
await ctx.enable_components(tags={"finance"})

# Also enable admin tools
await ctx.enable_components(tags={"admin"})

# Later: disable a specific admin tool
await ctx.disable_components(names={"dangerous_admin_tool"})

return "Session customized"
```

Each call adds a rule to the session. The `dangerous_admin_tool` ends up disabled because its disable rule was added after the admin enable rule.

### Filter Criteria

The session visibility methods accept the same filter criteria as `server.enable()` and `server.disable()`:

| Parameter | Description |
|-----------|-------------|
| `names` | Component names or URIs to match |
| `keys` | Component keys (e.g., `{"tool:my_tool"}`) |
| `tags` | Tags to match (component must have at least one) |
| `version` | Version specification to match |
| `components` | Component types (`{"tool"}`, `{"resource"}`, `{"prompt"}`, `{"template"}`) |
| `match_all` | If `True`, matches all components regardless of other criteria |

```python
from fastmcp.utilities.versions import VersionSpec

@mcp.tool
async def enable_recent_tools(ctx: Context) -> str:
"""Enable only tools from version 2.0.0 or later."""
await ctx.enable_components(
version=VersionSpec(gte="2.0.0"),
components={"tool"}
)
return "Recent tools enabled"
```

### Automatic Notifications

When session visibility changes, FastMCP automatically sends notifications to that session. Clients receive `ToolListChangedNotification`, `ResourceListChangedNotification`, and `PromptListChangedNotification` so they can refresh their component lists. These notifications go only to the affected session.

When you specify the `components` parameter, FastMCP optimizes by sending only the relevant notifications:

```python
# Only sends ToolListChangedNotification
await ctx.enable_components(tags={"finance"}, components={"tool"})

# Sends all three notifications (no components filter)
await ctx.enable_components(tags={"finance"})
```

### Namespace Activation Pattern

A common pattern organizes tools into namespaces using tag prefixes, disables them globally, then provides activation tools that unlock namespaces on demand:

```python
from fastmcp import FastMCP
from fastmcp.server.context import Context

server = FastMCP("Multi-Domain Assistant")

# Finance namespace
@server.tool(tags={"namespace:finance"})
def analyze_portfolio(symbols: list[str]) -> str:
return f"Analysis for: {', '.join(symbols)}"

@server.tool(tags={"namespace:finance"})
def get_market_data(symbol: str) -> dict:
return {"symbol": symbol, "price": 150.25}

# Admin namespace
@server.tool(tags={"namespace:admin"})
def list_users() -> list[str]:
return ["alice", "bob", "charlie"]

# Activation tools - always visible
@server.tool
async def activate_finance(ctx: Context) -> str:
await ctx.enable_components(tags={"namespace:finance"})
return "Finance tools activated"

@server.tool
async def activate_admin(ctx: Context) -> str:
await ctx.enable_components(tags={"namespace:admin"})
return "Admin tools activated"

@server.tool
async def deactivate_all(ctx: Context) -> str:
await ctx.reset_components()
return "All namespaces deactivated"

# Disable namespace tools globally
server.disable(tags={"namespace:finance", "namespace:admin"})
```
Comment thread
jlowin marked this conversation as resolved.

Sessions start seeing only the activation tools. Calling `activate_finance` reveals finance tools for that session only. Multiple namespaces can be activated independently, and `deactivate_all` returns to the initial state.

### Method Reference

- **`await ctx.enable_components(...) -> None`**: Enable matching components for this session
- **`await ctx.disable_components(...) -> None`**: Disable matching components for this session
- **`await ctx.reset_components() -> None`**: Clear all session rules, returning to global defaults

## Client Notifications

Expand Down
55 changes: 55 additions & 0 deletions examples/namespace_activation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Namespace Activation

Demonstrates session-specific visibility control using tags to organize tools into namespaces that can be activated on demand.

## Pattern

1. Tag tools with namespaces: `@server.tool(tags={"namespace:finance"})`
2. Globally disable namespaces: `server.disable(tags={"namespace:finance"})`
3. Provide activation tools that call `ctx.enable_components(tags={"namespace:finance"})`

Each session starts with only the activation tools visible. When a session calls an activation tool, that namespace becomes visible **only for that session**.

## Run

```bash
# Server
uv run python server.py

# Client (in another terminal)
uv run python client.py
```

## Example Output

```
Namespace Activation Demo

╭─────────────────── Initial Tools ───────────────────╮
│ activate_finance, activate_admin, deactivate_all │
╰─────────────────────────────────────────────────────╯

→ Calling activate_finance()
Finance tools activated
╭─────────────── After Activating Finance ────────────╮
│ analyze_portfolio, get_market_data, execute_trade, │
│ activate_finance, activate_admin, deactivate_all │
╰─────────────────────────────────────────────────────╯

→ Calling get_market_data(symbol='AAPL')
{'symbol': 'AAPL', 'price': 150.25, 'change': '+2.5%'}

→ Calling activate_admin()
Admin tools activated
╭────────────── After Activating Admin ───────────────╮
│ analyze_portfolio, get_market_data, execute_trade, │
│ list_users, reset_user_password, activate_finance, │
│ activate_admin, deactivate_all │
╰─────────────────────────────────────────────────────╯

→ Calling deactivate_all()
All namespaces deactivated
╭────────────── After Deactivating All ───────────────╮
│ activate_finance, activate_admin, deactivate_all │
╰─────────────────────────────────────────────────────╯
```
Comment on lines +25 to +55
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

Add a language tag to the example output fence.
markdownlint MD040 expects a language identifier on fenced blocks.

🔧 Suggested fix
-```
+```text
 Namespace Activation Demo
@@
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

76 changes: 76 additions & 0 deletions examples/namespace_activation/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Namespace Activation Client

Demonstrates how session-specific visibility works from the client perspective.
"""

import asyncio
import sys
from pathlib import Path

from rich import print
from rich.panel import Panel

from fastmcp import Client


def load_server():
"""Load the example server."""
examples_dir = Path(__file__).parent
if str(examples_dir) not in sys.path:
sys.path.insert(0, str(examples_dir))

import server as server_module

return server_module.server


server = load_server()


def show_tools(tools: list, title: str) -> None:
"""Display available tools in a panel."""
tool_names = [f"[cyan]{t.name}[/]" for t in tools]
print(Panel(", ".join(tool_names) or "[dim]No tools[/]", title=title))


async def main():
print("\n[bold]Namespace Activation Demo[/]\n")

async with Client(server) as client:
# Initially only activation tools are visible
tools = await client.list_tools()
show_tools(tools, "Initial Tools")

# Activate finance namespace
print("\n[yellow]→ Calling activate_finance()[/]")
result = await client.call_tool("activate_finance", {})
print(f" [green]{result.data}[/]")

tools = await client.list_tools()
show_tools(tools, "After Activating Finance")

# Use a finance tool
print("\n[yellow]→ Calling get_market_data(symbol='AAPL')[/]")
result = await client.call_tool("get_market_data", {"symbol": "AAPL"})
print(f" [green]{result.data}[/]")

# Activate admin namespace too
print("\n[yellow]→ Calling activate_admin()[/]")
result = await client.call_tool("activate_admin", {})
print(f" [green]{result.data}[/]")

tools = await client.list_tools()
show_tools(tools, "After Activating Admin")

# Deactivate all - back to defaults
print("\n[yellow]→ Calling deactivate_all()[/]")
result = await client.call_tool("deactivate_all", {})
print(f" [green]{result.data}[/]")

tools = await client.list_tools()
show_tools(tools, "After Deactivating All")


if __name__ == "__main__":
asyncio.run(main())
73 changes: 73 additions & 0 deletions examples/namespace_activation/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Namespace Activation Server

Tools are organized into namespaces using tags, globally disabled by default,
and selectively enabled per-session via activation tools.
"""

from fastmcp import FastMCP
from fastmcp.server.context import Context

server = FastMCP("Multi-Domain Assistant")


# Finance namespace
@server.tool(tags={"namespace:finance"})
def analyze_portfolio(symbols: list[str]) -> str:
"""Analyze a portfolio of stock symbols."""
return f"Portfolio analysis for: {', '.join(symbols)}"


@server.tool(tags={"namespace:finance"})
def get_market_data(symbol: str) -> dict:
"""Get current market data for a symbol."""
return {"symbol": symbol, "price": 150.25, "change": "+2.5%"}


@server.tool(tags={"namespace:finance"})
def execute_trade(symbol: str, quantity: int, side: str) -> str:
"""Execute a trade (simulated)."""
return f"Executed {side} order: {quantity} shares of {symbol}"


# Admin namespace
@server.tool(tags={"namespace:admin"})
def list_users() -> list[str]:
"""List all system users."""
return ["alice", "bob", "charlie"]


@server.tool(tags={"namespace:admin"})
def reset_user_password(username: str) -> str:
"""Reset a user's password (simulated)."""
return f"Password reset for {username}"


# Activation tools - always visible
@server.tool
async def activate_finance(ctx: Context) -> str:
"""Activate finance tools for this session."""
await ctx.enable_components(tags={"namespace:finance"})
return "Finance tools activated"


@server.tool
async def activate_admin(ctx: Context) -> str:
"""Activate admin tools for this session."""
await ctx.enable_components(tags={"namespace:admin"})
return "Admin tools activated"


@server.tool
async def deactivate_all(ctx: Context) -> str:
"""Deactivate all namespaces, returning to defaults."""
await ctx.reset_components()
return "All namespaces deactivated"


# Globally disable namespace tools by default
server.disable(tags={"namespace:finance", "namespace:admin"})


if __name__ == "__main__":
server.run()
Loading