Skip to content

Add session-specific visibility control via Context#2917

Merged
jlowin merged 10 commits intomainfrom
session-visibility
Jan 19, 2026
Merged

Add session-specific visibility control via Context#2917
jlowin merged 10 commits intomainfrom
session-visibility

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 18, 2026

Tools can now dynamically enable/disable components for their session only, with rules that override global transforms and persist across requests.

server = FastMCP("Multi-Domain Assistant")

@server.tool(tags={"namespace:finance"})
def analyze_portfolio(...): ...

@server.tool(tags={"namespace:finance"})  
def execute_trade(...): ...

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

@server.tool
async def activate_finance(ctx: Context):
    """Unlock finance tools for this session."""
    await ctx.enable_components(tags={"namespace:finance"})
    return "Finance tools activated"

Session A calls activate_finance() → sees finance tools.
Session B doesn't → still sees only basic tools.

API:

  • ctx.enable_components(names=..., tags=..., version=..., components=...)
  • ctx.disable_components(...)
  • ctx.reset_components() - clear session rules

Closes #2911

Tools can now enable/disable components for their session only using
ctx.enable_components() and ctx.disable_components(). Session rules
override global transforms and persist across requests within the session.
@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. labels Jan 18, 2026
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static analysis workflow failed because ruff auto-fixed an import ordering issue in examples/namespace_activation/client.py, but the pre-commit hook detected the modification and failed.

Root Cause: The imports in examples/namespace_activation/client.py:9-13 violate Python import ordering conventions (PEP 8). The local import from server import server appears before the third-party import from fastmcp import Client. Ruff's isort integration automatically reordered these imports, which causes the pre-commit hook to fail since it modifies files.

Suggested Solution:

Reorder the imports in examples/namespace_activation/client.py to follow standard Python conventions:

import asyncio

from rich import print
from rich.panel import Panel

from fastmcp import Client
from server import server

The proper order is:

  1. Standard library imports (asyncio)
  2. Third-party imports (rich, fastmcp)
  3. Local/relative imports (server)

After making this change, run uv run prek run --all-files locally to verify all checks pass before pushing.

Detailed Analysis

The CI log shows:

ruff check...............................................................Failed
- hook id: ruff-check
- exit code: 1
- files were modified by this hook

  Found 1 error (1 fixed, 0 remaining).

Running git diff on the branch shows ruff moved the import:

-from fastmcp import Client
 from server import server
+
+from fastmcp import Client

This is a standard import organization enforcement by ruff's I (isort) rules defined in pyproject.toml:L158.

Related Files
  • examples/namespace_activation/client.py:9-13 - File with import ordering issue
  • pyproject.toml:L158 - Ruff configuration that includes isort (I) checks
  • .pre-commit-config.yaml:L14-22 - Pre-commit hook configuration for ruff

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f14b0dc594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1091 to +1094
tools = await apply_session_transforms(tools)

# Then apply enabled filtering at the server level
tools = [t for t in await self.list_tools() if is_enabled(t)]
tools = [t for t in tools if is_enabled(t)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce enable/disable on direct component calls

Enabled filtering is now only applied in the list path (get_tools filters after apply_session_transforms), but call_tool/read_resource/render_prompt still call get_tool/get_resource/get_prompt, which (per Provider semantics) return disabled components without filtering and no longer have the FastMCP overrides. That means a client who already knows a disabled tool/resource/prompt name or URI can still execute/read/render it even after server.disable(...) or ctx.disable_components(...), defeating the intended access/visibility control. This is a behavior regression for disable-based gating and will allow hidden components to be invoked directly.

Useful? React with 👍 / 👎.

Comment on lines +1172 to +1176
# Send notifications based on components hint
if components is None or "tool" in components:
await self.send_notification(mcp.types.ToolListChangedNotification())
if components is None or "resource" in components:
await self.send_notification(mcp.types.ResourceListChangedNotification())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Notify resource list changes for template-only updates

Session visibility rules support components={'template'}, but _save_visibility_rules only emits tool/resource/prompt notifications. If a session enables/disables templates with components={'template'}, no resources/list_changed notification is sent, so clients that cache template lists will not refresh and will keep stale visibility. Since templates are listed via list_resource_templates but share the resources/list_changed channel, template-only updates should still trigger a resource list change notification.

Useful? React with 👍 / 👎.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Multiple tests failing because disabled components are not being filtered when fetching individual components by name/URI.

Root Cause: The PR accidentally deleted four critical override methods from the FastMCP class in src/fastmcp/server/server.py that filter disabled components:

  1. async def get_tool()
  2. async def get_resource()
  3. async def get_resource_template()
  4. async def get_prompt()

These methods call is_enabled() to filter out disabled components after all transforms are applied. The base Provider class has these methods but explicitly documents they do NOT filter disabled components - that's the responsibility of the FastMCP override. Without these overrides, the following test scenarios fail:

  • mcp.disable(names={"sample_prompt"}, components={"prompt"}) followed by mcp.get_prompt("sample_prompt") returns the prompt instead of None
  • Same issue for resources with tag filtering: disabled resources are returned instead of raising NotFoundError
  • Same issue for tools

Suggested Solution: Restore the four deleted override methods in src/fastmcp/server/server.py. These methods should be added back to the FastMCP class:

Detailed Analysis

Failed tests:

  • tests/server/providers/test_local_provider_prompts.py::TestPromptEnabled::test_get_prompt_and_disable (line 400)
  • tests/server/providers/test_local_provider_prompts.py::TestPromptEnabled::test_cant_get_disabled_prompt (line 413)
  • tests/server/providers/test_local_provider_prompts.py::TestPromptTags::test_read_prompt_includes_tags (line 463)
  • tests/server/providers/test_local_provider_prompts.py::TestPromptTags::test_read_prompt_excludes_tags (line 469)
  • tests/server/providers/test_local_provider_resources.py::TestResourceTags::test_read_included_resource (line 721)
  • tests/server/providers/test_local_provider_resources.py::TestResourceTags::test_read_excluded_resource (line 727)
  • Similar failures for tools

Error pattern:

AssertionError: assert FunctionPrompt(name='sample_prompt', ...) is None

The test expects None for a disabled component, but gets the actual component object.

Git diff evidence:

git diff main...origin/session-visibility -- src/fastmcp/server/server.py | grep -A 5 "^-.*async def get_"

Shows all four methods were deleted (lines starting with -).

Related Files

Primary file to fix:

  • src/fastmcp/server/server.py - Restore the four deleted get_* override methods in the FastMCP class

For reference (shows correct implementation):

  • src/fastmcp/server/providers/base.py - Base Provider class that documents these methods don't filter disabled components
  • src/fastmcp/server/transforms/enabled.py - The is_enabled() function used for filtering (lines 273-290)

Test files showing expected behavior:

  • tests/server/providers/test_local_provider_prompts.py (lines 395-413)
  • tests/server/providers/test_local_provider_resources.py (lines 716-727)
  • tests/server/providers/test_local_provider_tools.py (similar patterns)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

Warning

Rate limit exceeded

@jlowin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3563c99 and e2c14a6.

📒 Files selected for processing (1)
  • src/fastmcp/server/context.py

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds per-session visibility control to Context: async methods enable_components, disable_components, and reset_components persist visibility rules in session state and translate them into Enabled transforms. Introduces apply_session_transforms to convert session rules into transforms and apply them when assembling component lists. Server listing/getter methods now apply session transforms before filtering by is_enabled. Adjusts Enabled and provider enable/disable APIs to use set-based typing. Adds an examples namespace-activation server and client demonstrating session-scoped activation of tagged tools.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks the required Contributors and Review Checklists; critical items like testing, documentation updates, and self-review confirmation are missing. Add the complete Contributors and Review Checklists to the PR description to confirm all development and testing requirements have been met.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add session-specific visibility control via Context' clearly and concisely describes the main feature introduced in the PR.
Linked Issues check ✅ Passed All primary objectives from #2911 are met: session-scoped visibility rules stored in _visibility_rules, session rules override global transforms, visibility notifications sent per-session, and the new Context API (enable_components, disable_components, reset_components) fully addresses namespace activation and progressive disclosure patterns.
Out of Scope Changes check ✅ Passed All changes directly support the PR's core objective of adding session-specific visibility control; no extraneous modifications were introduced outside the scope of #2911.
Docstring Coverage ✅ Passed Docstring coverage is 91.43% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/fastmcp/server/transforms/enabled.py (1)

61-90: Defensively copy mutable criteria sets.
Storing caller-owned sets allows later mutation to change transform behavior unexpectedly.

♻️ Suggested fix
-        self.names = names
-        self.keys = keys
+        self.names = set(names) if names else None
+        self.keys = set(keys) if keys else None
         self.version = version
-        self.tags = tags  # e.g., {"internal", "deprecated"}
-        self.components = components  # e.g., {"tool", "prompt"}
+        self.tags = set(tags) if tags else None  # e.g., {"internal", "deprecated"}
+        self.components = set(components) if components else None  # e.g., {"tool", "prompt"}

Comment on lines +25 to +55
```
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 │
╰─────────────────────────────────────────────────────╯
```
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)

Comment thread src/fastmcp/server/server.py Outdated
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static analysis check failed because ruff auto-fixed an import ordering violation in the new example file, but the pre-commit hook rejects changes made during CI runs.

Root Cause: In examples/namespace_activation/client.py, the imports are not following the correct sorting order. Ruff's import sorter (isort) expects local imports (from server import server) to come before third-party library imports (from fastmcp import Client) when they're in the same group after the blank line separator.

The pre-commit hook ran ruff, which automatically fixed this ordering issue, but because files were modified during the check, the workflow failed with exit code 1. This is the expected behavior - the code should be properly formatted before pushing.

Suggested Solution: Run uv run prek run --all-files locally on the session-visibility branch to apply the formatting fixes, then commit the changes. Specifically, the import order in examples/namespace_activation/client.py needs to be:

from rich import print
from rich.panel import Panel
from server import server

from fastmcp import Client

This follows the standard import grouping: standard library → third-party (rich) → local modules (server) → blank line → project imports (fastmcp).

Detailed Analysis

Relevant Log Excerpt

static_analysis  Run prek  2026-01-19T02:31:30.6409339Z ruff check...............................................................[41mFailed[49m
static_analysis  Run prek  2026-01-19T02:31:30.6417724Z [2m- hook id: ruff-check[0m
static_analysis  Run prek  2026-01-19T02:31:30.6418298Z [2m- exit code: 1[0m
static_analysis  Run prek  2026-01-19T02:31:30.6418772Z [2m- files were modified by this hook[0m
static_analysis  Run prek  2026-01-19T02:31:30.6419082Z 
static_analysis  Run prek  2026-01-19T02:31:30.6419242Z   Found 1 error (1 fixed, 0 remaining).

The Fix Applied by Ruff

--- a/examples/namespace_activation/client.py
+++ b/examples/namespace_activation/client.py
@@ -8,9 +8,9 @@ import asyncio
 
 from rich import print
 from rich.panel import Panel
+from server import server
 
 from fastmcp import Client
-from server import server
Related Files
  • examples/namespace_activation/client.py - New file with import ordering issue
  • .pre-commit-config.yaml - Defines the ruff-check hook that caught this issue
  • pyproject.toml - Contains ruff configuration for import sorting rules

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Test test_token_refresh_with_mock_provider times out on Windows during OAuthProxy initialization when creating a default DiskStore.

Root Cause: The test at tests/server/auth/test_oauth_proxy.py:809 creates an OAuthProxy without providing a client_storage parameter. This causes the proxy to initialize a default DiskStore (via diskcache and SQLite) at line 822 of oauth_proxy.py. On Windows, the SQLite database initialization is blocking/hanging in the test environment, causing the 5-second timeout.

The stack trace shows the test is stuck in:

test_oauth_proxy.py:840 - with patch("fastmcp.server.auth.oauth_proxy.AsyncOAuth2Client")
  → oauth_proxy.py:896 - logger.debug() inside __init__
    → DiskStore.__init__
      → diskcache connection initialization (blocking on Windows)

Suggested Solution:

Pass a MemoryStore() as the client_storage parameter to avoid SQLite initialization in tests. This pattern is already used in other OAuth tests in the same directory.

Change line 811-819 in tests/server/auth/test_oauth_proxy.py from:

proxy = OAuthProxy(
    upstream_authorization_endpoint=mock_oauth_provider.authorize_endpoint,
    upstream_token_endpoint=mock_oauth_provider.token_endpoint,
    upstream_client_id="mock-client",
    upstream_client_secret="mock-secret",
    token_verifier=MockTokenVerifier(),
    base_url="http://localhost:8000",
    jwt_signing_key="test-secret",
)

To:

from key_value.aio.stores.memory import MemoryStore

proxy = OAuthProxy(
    upstream_authorization_endpoint=mock_oauth_provider.authorize_endpoint,
    upstream_token_endpoint=mock_oauth_provider.token_endpoint,
    upstream_client_id="mock-client",
    upstream_client_secret="mock-secret",
    token_verifier=MockTokenVerifier(),
    base_url="http://localhost:8000",
    jwt_signing_key="test-secret",
    client_storage=MemoryStore(),  # Use in-memory storage to avoid Windows DiskStore timeout
)
Detailed Analysis

Failed Test Log Excerpt

tests\server\auth\test_oauth_proxy.py::TestOAuthProxyTokenRefresh::test_token_refresh_with_mock_provider
+++++++++++++++++++++++++++++++++++ Timeout +++++++++++++++++++++++++++++++++++
~~~~~~~~~~~~~~~~~~~~~~~~~~ Stack of MainThread (1692) ~~~~~~~~~~~~~~~~~~~~~~~~~~
  File "D:\a\fastmcp\fastmcp\tests\server\auth\test_oauth_proxy.py", line 840
    with patch("fastmcp.server.auth.oauth_proxy.AsyncOAuth2Client") as MockClient:
  File "D:\a\fastmcp\fastmcp\src\fastmcp\server\auth\oauth_proxy.py", line 896
    logger.debug(...)
  File "D:\a\fastmcp\fastmcp\.venv\lib\site-packages\key_value\aio\stores\disk\store.py", line 83
    super().__init__(...)
  File "D:\a\fastmcp\fastmcp\.venv\lib\site-packages\diskcache\core.py", line 591
    self._sql  # Database initialization

The test hangs when trying to initialize the SQLite database for DiskStore on Windows.

Examples of Correct Pattern

Other tests in the same directory already use this pattern:

  • tests/server/auth/test_oauth_consent_flow.py - Lines 66, 80, 95, 688, 761, 829, 904, 959, 1015
  • tests/server/auth/test_oauth_proxy_storage.py - Lines 40, 56, 124, 147, 166

All of these pass client_storage=MemoryStore() to avoid DiskStore initialization issues in tests.

Related Files
  • tests/server/auth/test_oauth_proxy.py:809-819 - Test that needs fixing
  • src/fastmcp/server/auth/oauth_proxy.py:813-824 - Where DiskStore is created by default
  • tests/server/auth/test_oauth_consent_flow.py - Examples of correct MemoryStore usage
  • tests/server/auth/test_oauth_proxy_storage.py - More examples of correct pattern

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Comment thread docs/servers/enabled.mdx
Comment thread docs/servers/enabled.mdx
Comment thread src/fastmcp/server/context.py
@jlowin jlowin merged commit d8ab493 into main Jan 19, 2026
11 checks passed
@jlowin jlowin deleted the session-visibility branch January 19, 2026 03:31
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Jan 30, 2026
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add per-session visibility via Context

1 participant