Skip to content

Commit f2b0430

Browse files
committed
add app-level config for exposing callback docstrings in MCP tools
1 parent 463ee30 commit f2b0430

3 files changed

Lines changed: 49 additions & 0 deletions

File tree

dash/_configs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def load_dash_env_vars():
3434
"DASH_COMPRESS",
3535
"DASH_MCP_ENABLED",
3636
"DASH_MCP_PATH",
37+
"DASH_MCP_EXPOSE_DOCSTRINGS",
3738
"HOST",
3839
"PORT",
3940
)

dash/dash.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ def __init__( # pylint: disable=too-many-statements
485485
csrf_header_name: str = "X-CSRFToken",
486486
enable_mcp: Optional[bool] = None,
487487
mcp_path: Optional[str] = None,
488+
mcp_expose_docstrings: Optional[bool] = None,
488489
**obsolete,
489490
):
490491

@@ -565,6 +566,9 @@ def __init__( # pylint: disable=too-many-statements
565566
hide_all_callbacks=False,
566567
csrf_token_name=csrf_token_name,
567568
csrf_header_name=csrf_header_name,
569+
mcp_expose_docstrings=get_combined_config(
570+
"mcp_expose_docstrings", mcp_expose_docstrings, False
571+
),
568572
)
569573
self.config.set_read_only(
570574
[

tests/unit/mcp/tools/test_callback_adapter.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,50 @@ def update(val):
244244
in tool.description
245245
)
246246

247+
def test_app_level_opt_in_exposes_docstrings(self):
248+
"""Dash(mcp_expose_docstrings=True) exposes docstrings for all callbacks."""
249+
app = Dash(__name__, mcp_expose_docstrings=True)
250+
app.layout = html.Div([dcc.Input(id="inp"), html.Div(id="out")])
251+
252+
@app.callback(Output("out", "children"), Input("inp", "value"))
253+
def update(val):
254+
"""intentionally-exposed callback docstring text for the LLM"""
255+
return val
256+
257+
app_context.set(app)
258+
app.mcp_callback_map = CallbackAdapterCollection(app)
259+
260+
with app.server.test_request_context():
261+
tool = app.mcp_callback_map[0].as_mcp_tool
262+
assert (
263+
"intentionally-exposed callback docstring text for the LLM"
264+
in tool.description
265+
)
266+
267+
def test_per_callback_false_overrides_app_level_opt_in(self):
268+
"""Per-callback mcp_expose_docstring=False wins over app-level opt-in."""
269+
app = Dash(__name__, mcp_expose_docstrings=True)
270+
app.layout = html.Div([dcc.Input(id="inp"), html.Div(id="out")])
271+
272+
@app.callback(
273+
Output("out", "children"),
274+
Input("inp", "value"),
275+
mcp_expose_docstring=False,
276+
)
277+
def update(val):
278+
"""sensitive callback docstring text that must not leak to LLMs"""
279+
return val
280+
281+
app_context.set(app)
282+
app.mcp_callback_map = CallbackAdapterCollection(app)
283+
284+
with app.server.test_request_context():
285+
tool = app.mcp_callback_map[0].as_mcp_tool
286+
assert (
287+
"sensitive callback docstring text that must not leak to LLMs"
288+
not in tool.description
289+
)
290+
247291
def test_description_includes_output_target(self, simple_app):
248292
with simple_app.server.test_request_context():
249293
tool = app_context.get().mcp_callback_map[0].as_mcp_tool

0 commit comments

Comments
 (0)