Skip to content
Merged
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)

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

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

import asyncio

from rich import print
from rich.panel import Panel

from fastmcp import Client
from server import 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()
16 changes: 8 additions & 8 deletions loq.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ max_lines = 1899

[[rules]]
path = "tests/server/middleware/test_middleware.py"
max_lines = 1250
max_lines = 1070

[[rules]]
path = "src/fastmcp/server/context.py"
Expand All @@ -40,7 +40,7 @@ max_lines = 1748

[[rules]]
path = "tests/server/test_mount.py"
max_lines = 1560
max_lines = 1545

[[rules]]
path = "tests/utilities/test_inspect.py"
Expand All @@ -60,15 +60,15 @@ max_lines = 3250

[[rules]]
path = "tests/tools/test_tool.py"
max_lines = 2250
max_lines = 2026

[[rules]]
path = "tests/client/test_elicitation.py"
max_lines = 1132

[[rules]]
path = "src/fastmcp/client/client.py"
max_lines = 2000
max_lines = 1885

[[rules]]
path = "tests/utilities/test_json_schema_type.py"
Expand All @@ -95,9 +95,9 @@ path = "tests/server/auth/test_jwt_provider.py"
max_lines = 1101

[[rules]]
path = "docs/servers/tools.mdx"
max_lines = 1200
path = "src/fastmcp/server/providers/local_provider.py"
max_lines = 1187

[[rules]]
path = "docs/changelog.mdx"
max_lines = 2280
path = "tests/server/test_versioning.py"
max_lines = 1235
Loading
Loading