Skip to content

Commit c5c4829

Browse files
committed
Address PR feedback
1 parent 52b2858 commit c5c4829

5 files changed

Lines changed: 173 additions & 29 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [UNRELEASED]
66

7+
## [4.3.0rc1] - Unreleased
8+
9+
## Added
10+
- [#3796](https://github.com/plotly/dash/pull/3796) MCP: Add `configure_mcp_server()` to toggle which content the MCP server exposes (`include_layout`, `include_callbacks`, `include_clientside_callbacks`, `include_pages`, `expose_callback_docstrings`). Only the parameters explicitly passed are updated; omitted parameters retain their current value.
11+
12+
## Changed
13+
- [#3796](https://github.com/plotly/dash/pull/3796) MCP: Remove the `mcp_expose_docstrings` `Dash()` constructor argument; callback docstring exposure is now controlled via `configure_mcp_server(expose_callback_docstrings=...)`.
14+
715
## [4.3.0rc0] - 2026-05-21
816

917
## Added

‎dash/mcp/_configure.py‎

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from __future__ import annotations
88

9+
from typing import Optional
10+
911
from dash import get_app
1012
from dash.exceptions import AppNotFoundError
1113
from dash.mcp.primitives.resources import _RESOURCE_PROVIDERS as MCP_RESOURCE_PROVIDERS
@@ -23,20 +25,37 @@
2325
_ALL_MCP_RESOURCE_PROVIDERS = list(MCP_RESOURCE_PROVIDERS)
2426
_ALL_MCP_TOOL_PROVIDERS = list(MCP_TOOL_PROVIDERS)
2527

28+
# Membership groupings (order-independent): which providers each toggle
29+
# controls. The exposed order is owned solely by the registry lists.
30+
_LAYOUT_RESOURCES = {LayoutResource, ComponentsResource}
31+
_CLIENTSIDE_CALLBACK_RESOURCES = {ClientsideCallbacksResource}
32+
_PAGE_RESOURCES = {PagesResource, PageLayoutResource}
33+
_LAYOUT_TOOLS = {GetDashComponentTool}
34+
35+
_DEFAULT_CONFIG = {
36+
"include_layout": True,
37+
"include_callbacks": True,
38+
"include_clientside_callbacks": True,
39+
"include_pages": True,
40+
"expose_callback_docstrings": False,
41+
}
42+
_current_config = dict(_DEFAULT_CONFIG)
43+
2644

2745
def configure_mcp_server(
2846
*,
29-
include_layout: bool = True,
30-
include_callbacks: bool = True,
31-
include_clientside_callbacks: bool = True,
32-
include_pages: bool = True,
33-
expose_callback_docstrings: bool = False,
47+
include_layout: Optional[bool] = None,
48+
include_callbacks: Optional[bool] = None,
49+
include_clientside_callbacks: Optional[bool] = None,
50+
include_pages: Optional[bool] = None,
51+
expose_callback_docstrings: Optional[bool] = None,
3452
) -> None:
3553
"""
3654
Configure which content the Dash MCP server exposes.
3755
38-
Any parameter that is omitted will be reset to its default value. Calling
39-
with no args will reset all configuration to its default state.
56+
Only the parameters that are explicitly passed are updated; any parameter
57+
that is omitted keeps its current value. On the first call, unset values
58+
take their defaults (all content included except callback docstrings).
4059
4160
:param include_layout: Expose ``dash://layout``, ``dash://components``,
4261
and the ``get_dash_component`` tool. Defaults to ``True``.
@@ -66,25 +85,49 @@ def configure_mcp_server(
6685
if get_app().backend.has_request_context():
6786
raise RuntimeError("MCP server can't be configured within a callback")
6887
except AppNotFoundError:
69-
...
70-
71-
CallbackTools.callbacks_mcp_enabled_by_default = include_callbacks
72-
CallbackTools.expose_docstrings_by_default = expose_callback_docstrings
73-
74-
updated_resources = list(_ALL_MCP_RESOURCE_PROVIDERS)
75-
if not include_layout:
76-
updated_resources.remove(LayoutResource)
77-
updated_resources.remove(ComponentsResource)
78-
if not include_clientside_callbacks:
79-
updated_resources.remove(ClientsideCallbacksResource)
80-
if not include_pages:
81-
updated_resources.remove(PagesResource)
82-
updated_resources.remove(PageLayoutResource)
83-
MCP_RESOURCE_PROVIDERS[:] = updated_resources
84-
85-
updated_tools = list(_ALL_MCP_TOOL_PROVIDERS)
86-
if not include_layout:
87-
updated_tools.remove(GetDashComponentTool)
88-
MCP_TOOL_PROVIDERS[:] = updated_tools
89-
90-
get_app().mcp_callback_map = None
88+
pass
89+
90+
passed = {
91+
"include_layout": include_layout,
92+
"include_callbacks": include_callbacks,
93+
"include_clientside_callbacks": include_clientside_callbacks,
94+
"include_pages": include_pages,
95+
"expose_callback_docstrings": expose_callback_docstrings,
96+
}
97+
_current_config.update(
98+
{key: value for key, value in passed.items() if value is not None}
99+
)
100+
101+
CallbackTools.callbacks_mcp_enabled_by_default = _current_config[
102+
"include_callbacks"
103+
]
104+
CallbackTools.expose_docstrings_by_default = _current_config[
105+
"expose_callback_docstrings"
106+
]
107+
108+
excluded_resources: set = set()
109+
if not _current_config["include_layout"]:
110+
excluded_resources |= _LAYOUT_RESOURCES
111+
if not _current_config["include_clientside_callbacks"]:
112+
excluded_resources |= _CLIENTSIDE_CALLBACK_RESOURCES
113+
if not _current_config["include_pages"]:
114+
excluded_resources |= _PAGE_RESOURCES
115+
MCP_RESOURCE_PROVIDERS[:] = [
116+
resource
117+
for resource in _ALL_MCP_RESOURCE_PROVIDERS
118+
if resource not in excluded_resources
119+
]
120+
121+
excluded_tools: set = set()
122+
if not _current_config["include_layout"]:
123+
excluded_tools |= _LAYOUT_TOOLS
124+
MCP_TOOL_PROVIDERS[:] = [
125+
tool for tool in _ALL_MCP_TOOL_PROVIDERS if tool not in excluded_tools
126+
]
127+
128+
# Invalidate the cached callback map so it is rebuilt with the new config.
129+
# No app yet (configured before `Dash()`) means there is no cache to clear.
130+
try:
131+
get_app().mcp_callback_map = None
132+
except AppNotFoundError:
133+
pass

‎tests/integration/mcp/conftest.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
from dash.mcp.primitives.tools.tools_callbacks import ( # pylint: disable=wrong-import-position
2121
CallbackTools,
2222
)
23+
from dash.mcp._decorator import ( # pylint: disable=wrong-import-position
24+
MCP_DECORATED_FUNCTIONS,
25+
)
26+
from dash.mcp import _configure # pylint: disable=wrong-import-position
2327

2428

2529
@pytest.fixture(autouse=True)
@@ -42,6 +46,8 @@ def _reset_dash_app_state():
4246
_TOOL_PROVIDERS[:] = initial_tools
4347
CallbackTools.callbacks_mcp_enabled_by_default = initial_callbacks_default
4448
CallbackTools.expose_docstrings_by_default = initial_expose_docstrings
49+
MCP_DECORATED_FUNCTIONS.clear()
50+
_configure._current_config = dict(_configure._DEFAULT_CONFIG)
4551
_get_app.APP = None
4652
_get_app.app_context.set(None)
4753

‎tests/integration/mcp/test_mcp_configure.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,30 @@ def included(val):
9393
resources = _mcp_method(dash_duo.server.url, "resources/list")
9494
uris = [r["uri"] for r in resources["result"]["resources"]]
9595
assert "dash://layout" in uris
96+
97+
98+
def test_mcpcfg004_configure_before_app_governs_live_server(dash_duo):
99+
"""configure_mcp_server() called BEFORE Dash() still governs the running
100+
server: app construction and the first request do not reset the config."""
101+
# Configure before the app exists.
102+
configure_mcp_server(include_layout=False, include_callbacks=False)
103+
104+
app = Dash(__name__)
105+
app.layout = html.Div([dcc.Input(id="inp"), html.Div(id="out")])
106+
107+
@app.callback(Output("out", "children"), Input("inp", "value"), mcp_enabled=True)
108+
def included(val):
109+
return val
110+
111+
dash_duo.start_server(app)
112+
113+
# First real request builds the callback map; pre-app config must hold.
114+
tools = _mcp_tools(dash_duo.server.url)
115+
tool_names = [t["name"] for t in tools]
116+
assert "included" in tool_names # opt-in honored under include_callbacks=False
117+
assert "get_dash_component" not in tool_names # include_layout=False honored
118+
119+
resources = _mcp_method(dash_duo.server.url, "resources/list")
120+
uris = [r["uri"] for r in resources["result"]["resources"]]
121+
assert "dash://layout" not in uris
122+
assert "dash://components" not in uris

‎tests/unit/mcp/test_mcp_configure.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ def _reset_mcp_module_state():
4646
"""Restore module-level MCP state after every test."""
4747
yield
4848

49+
from dash.mcp import _configure
50+
4951
CallbackTools.callbacks_mcp_enabled_by_default = True
5052
CallbackTools.expose_docstrings_by_default = False
5153
_RESOURCE_PROVIDERS[:] = list(_DEFAULT_RESOURCE_PROVIDERS)
5254
_TOOL_PROVIDERS[:] = list(_DEFAULT_TOOL_PROVIDERS)
55+
_configure._current_config = dict(_configure._DEFAULT_CONFIG)
5356
_get_app_module.APP = None
5457
_get_app_module.app_context.set(None)
5558

@@ -220,3 +223,60 @@ def test_mcpc007_configure_mcp_invalidates_mcp_callback_map():
220223
configure_mcp_server(include_callbacks=False)
221224

222225
assert app.mcp_callback_map is None
226+
227+
228+
# ---------------------------------------------------------------------------
229+
# Patch-style semantics: omitted params retain their previous value
230+
# ---------------------------------------------------------------------------
231+
232+
233+
def test_mcpc009_omitted_params_retain_previous_value():
234+
"""A subsequent call only updates the params it is passed; previously-set
235+
values persist instead of resetting to their defaults."""
236+
_make_app()
237+
238+
configure_mcp_server(include_layout=False)
239+
assert LayoutResource not in _RESOURCE_PROVIDERS
240+
assert GetDashComponentTool not in _TOOL_PROVIDERS
241+
242+
# Touch an unrelated knob; include_layout must stay disabled.
243+
configure_mcp_server(include_pages=False)
244+
245+
assert LayoutResource not in _RESOURCE_PROVIDERS
246+
assert GetDashComponentTool not in _TOOL_PROVIDERS
247+
assert PagesResource not in _RESOURCE_PROVIDERS
248+
249+
250+
# ---------------------------------------------------------------------------
251+
# Configuring before the app survives construction and first-request build
252+
# ---------------------------------------------------------------------------
253+
254+
255+
def test_mcpc010_configure_before_app_survives_construction():
256+
"""Config applied before `Dash()` exists is not reset by `Dash.__init__`:
257+
provider lists set pre-app remain in effect after construction."""
258+
# Configure first, with no app in context (must not raise).
259+
configure_mcp_server(include_layout=False)
260+
261+
# Constructing the app must not restore the disabled providers.
262+
_make_app()
263+
264+
assert LayoutResource not in _RESOURCE_PROVIDERS
265+
assert ComponentsResource not in _RESOURCE_PROVIDERS
266+
assert GetDashComponentTool not in _TOOL_PROVIDERS
267+
268+
269+
def test_mcpc011_configure_before_app_governs_callback_filter_mode():
270+
"""`include_callbacks=False` set before `Dash()` still governs how the
271+
callback map is built on first request (opt-in mode is honored)."""
272+
# Configure opt-in mode before any app exists.
273+
configure_mcp_server(include_callbacks=False)
274+
275+
# App built afterward; its single callback does not opt in.
276+
app_none = _make_app() # mcp_enabled defaults to None
277+
# The collection is built lazily (as on first request) and must reflect
278+
# the pre-app opt-in config: the non-opted-in callback is excluded.
279+
assert len(_collection(app_none)) == 0
280+
281+
app_opted_in = _make_app(mcp_enabled=True)
282+
assert len(_collection(app_opted_in)) == 1

0 commit comments

Comments
 (0)