-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Implement configuration function to customize MCP server #3796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mcp
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| """Dash MCP (Model Context Protocol) server integration.""" | ||
|
|
||
| from dash.mcp._configure import configure_mcp_server | ||
| from dash.mcp._decorator import mcp_enabled | ||
| from dash.mcp._server import enable_mcp_server | ||
|
|
||
| __all__ = [ | ||
| "configure_mcp_server", | ||
| "enable_mcp_server", | ||
| "mcp_enabled", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||
| """Public configuration API for the Dash MCP server.""" | ||||||
|
|
||||||
| # pylint: disable=cyclic-import | ||||||
| # dash.dash lazy-imports dash.mcp inside _setup_routes(); pylint's static | ||||||
| # analysis treats it as a module-level import, producing a false cycle. | ||||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| from dash import get_app | ||||||
| from dash.exceptions import AppNotFoundError | ||||||
| from dash.mcp.primitives.resources import _RESOURCE_PROVIDERS as MCP_RESOURCE_PROVIDERS | ||||||
| from dash.mcp.primitives.resources.resource_clientside_callbacks import ( | ||||||
| ClientsideCallbacksResource, | ||||||
| ) | ||||||
| from dash.mcp.primitives.resources.resource_components import ComponentsResource | ||||||
| from dash.mcp.primitives.resources.resource_layout import LayoutResource | ||||||
| from dash.mcp.primitives.resources.resource_page_layout import PageLayoutResource | ||||||
| from dash.mcp.primitives.resources.resource_pages import PagesResource | ||||||
| from dash.mcp.primitives.tools import _TOOL_PROVIDERS as MCP_TOOL_PROVIDERS | ||||||
| from dash.mcp.primitives.tools.tool_get_dash_component import GetDashComponentTool | ||||||
| from dash.mcp.primitives.tools.tools_callbacks import CallbackTools | ||||||
|
|
||||||
| _ALL_MCP_RESOURCE_PROVIDERS = list(MCP_RESOURCE_PROVIDERS) | ||||||
| _ALL_MCP_TOOL_PROVIDERS = list(MCP_TOOL_PROVIDERS) | ||||||
|
|
||||||
|
|
||||||
| def configure_mcp_server( | ||||||
| *, | ||||||
| include_layout: bool = True, | ||||||
| include_callbacks: bool = True, | ||||||
| include_clientside_callbacks: bool = True, | ||||||
| include_pages: bool = True, | ||||||
| expose_callback_docstrings: bool = False, | ||||||
| ) -> None: | ||||||
| """ | ||||||
| Configure which content the Dash MCP server exposes. | ||||||
|
|
||||||
| Any parameter that is omitted will be reset to its default value. Calling | ||||||
| with no args will reset all configuration to its default state. | ||||||
|
Comment on lines
+38
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, it seems safer to only mutate the config values when they're specifically passed in. On first call they get set with defaults or the provided values, then subsequent calls only update the specific values passed in. |
||||||
|
|
||||||
| :param include_layout: Expose ``dash://layout``, ``dash://components``, | ||||||
| and the ``get_dash_component`` tool. Defaults to ``True``. | ||||||
| :param include_callbacks: When ``True`` (default), all callbacks are | ||||||
| included; ``mcp_enabled=False`` on a ``@callback`` opts it out. | ||||||
| When ``False``, no callbacks are included by default; | ||||||
| ``mcp_enabled=True`` opts a specific callback in. | ||||||
| :param include_clientside_callbacks: Expose the | ||||||
| ``dash://clientside-callbacks`` resource. Defaults to ``True``. | ||||||
| :param include_pages: Expose ``dash://pages`` and | ||||||
| ``dash://page-layout/{path}``. Defaults to ``True``. | ||||||
| :param expose_callback_docstrings: Include callback docstrings in | ||||||
| tool descriptions. Defaults to ``False``. | ||||||
|
|
||||||
| Example — expose only ``@mcp_enabled``-decorated functions:: | ||||||
|
|
||||||
| from dash.mcp import configure_mcp_server | ||||||
|
|
||||||
| configure_mcp_server( | ||||||
| include_layout=False, | ||||||
| include_callbacks=False, | ||||||
| include_clientside_callbacks=False, | ||||||
| include_pages=False, | ||||||
| ) | ||||||
| """ | ||||||
| try: | ||||||
| if get_app().backend.has_request_context(): | ||||||
| raise RuntimeError("MCP server can't be configured within a callback") | ||||||
| except AppNotFoundError: | ||||||
| ... | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| CallbackTools.callbacks_mcp_enabled_by_default = include_callbacks | ||||||
| CallbackTools.expose_docstrings_by_default = expose_callback_docstrings | ||||||
|
|
||||||
| updated_resources = list(_ALL_MCP_RESOURCE_PROVIDERS) | ||||||
| if not include_layout: | ||||||
| updated_resources.remove(LayoutResource) | ||||||
| updated_resources.remove(ComponentsResource) | ||||||
| if not include_clientside_callbacks: | ||||||
| updated_resources.remove(ClientsideCallbacksResource) | ||||||
| if not include_pages: | ||||||
| updated_resources.remove(PagesResource) | ||||||
| updated_resources.remove(PageLayoutResource) | ||||||
|
Comment on lines
+75
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of creating lists to keep things tidy? Something like this: LAYOUT_RESOURCES = [ComponentsResource, LayoutResource]
if not include_layout:
updated_resources.remove(item) for item in LAYOUT_RESOURCES |
||||||
| MCP_RESOURCE_PROVIDERS[:] = updated_resources | ||||||
|
|
||||||
| updated_tools = list(_ALL_MCP_TOOL_PROVIDERS) | ||||||
| if not include_layout: | ||||||
| updated_tools.remove(GetDashComponentTool) | ||||||
| MCP_TOOL_PROVIDERS[:] = updated_tools | ||||||
|
|
||||||
| get_app().mcp_callback_map = None | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to wrap this in a try/except block too? |
||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to reset app_context.set(app)
if MCP_DECORATED_FUNCTIONS:
app.mcp_decorated_functions = dict(MCP_DECORATED_FUNCTIONS)
MCP_DECORATED_FUNCTIONS.clear() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| """Integration tests for configure_mcp().""" | ||
|
|
||
| from dash import Dash, Input, Output, dcc, html | ||
| from dash.mcp import configure_mcp_server, mcp_enabled | ||
|
|
||
| from tests.integration.mcp.conftest import _mcp_method, _mcp_tools | ||
|
|
||
|
|
||
| def test_mcpcfg001_disable_everything_decorated_function_still_appears(dash_duo): | ||
| """configure_mcp with all content disabled: layout/callback/page resources and | ||
| tools are absent, but an @mcp_enabled decorated function still appears.""" | ||
|
|
||
| @mcp_enabled | ||
| def my_tool(x: int) -> int: | ||
| return x * 2 | ||
|
|
||
| app = Dash(__name__) | ||
| app.layout = html.Div([dcc.Input(id="inp"), html.Div(id="out")]) | ||
|
|
||
| @app.callback(Output("out", "children"), Input("inp", "value")) | ||
| def update(val): | ||
| return val | ||
|
|
||
| configure_mcp_server( | ||
| include_layout=False, | ||
| include_callbacks=False, | ||
| include_clientside_callbacks=False, | ||
| include_pages=False, | ||
| ) | ||
| dash_duo.start_server(app) | ||
|
|
||
| tools = _mcp_tools(dash_duo.server.url) | ||
| tool_names = [t["name"] for t in tools] | ||
| assert "update" not in tool_names | ||
| assert "get_dash_component" not in tool_names | ||
| assert tool_names == ["my_tool"] | ||
|
|
||
| resources = _mcp_method(dash_duo.server.url, "resources/list") | ||
| uris = [r["uri"] for r in resources["result"]["resources"]] | ||
| assert "dash://layout" not in uris | ||
| assert "dash://components" not in uris | ||
| assert "dash://clientside-callbacks" not in uris | ||
|
|
||
|
|
||
| def test_mcpcfg002_disable_layout_callbacks_still_appear(dash_duo): | ||
| """configure_mcp(include_layout=False): callback tools are present, | ||
| get_dash_component is absent, layout resources are absent.""" | ||
| app = Dash(__name__) | ||
| app.layout = html.Div([dcc.Input(id="inp"), html.Div(id="out")]) | ||
|
|
||
| @app.callback(Output("out", "children"), Input("inp", "value")) | ||
| def update(val): | ||
| return val | ||
|
|
||
| configure_mcp_server(include_layout=False) | ||
| dash_duo.start_server(app) | ||
|
|
||
| tools = _mcp_tools(dash_duo.server.url) | ||
| tool_names = [t["name"] for t in tools] | ||
| assert "update" in tool_names | ||
| assert "get_dash_component" not in tool_names | ||
|
|
||
| resources = _mcp_method(dash_duo.server.url, "resources/list") | ||
| uris = [r["uri"] for r in resources["result"]["resources"]] | ||
| assert "dash://layout" not in uris | ||
| assert "dash://components" not in uris | ||
|
|
||
|
|
||
| def test_mcpcfg003_disable_callbacks_single_opt_in_layout_queryable(dash_duo): | ||
| """configure_mcp(include_callbacks=False) with one explicit mcp_enabled=True | ||
| callback: only that callback appears as a tool, layout is queryable.""" | ||
| app = Dash(__name__) | ||
| app.layout = html.Div( | ||
| [dcc.Input(id="inp"), html.Div(id="out"), html.Div(id="out2")] | ||
| ) | ||
|
|
||
| @app.callback(Output("out", "children"), Input("inp", "value")) | ||
| def excluded(val): | ||
| return val | ||
|
|
||
| @app.callback(Output("out2", "children"), Input("inp", "value"), mcp_enabled=True) | ||
| def included(val): | ||
| return val | ||
|
|
||
| configure_mcp_server(include_callbacks=False) | ||
| dash_duo.start_server(app) | ||
|
|
||
| tools = _mcp_tools(dash_duo.server.url) | ||
| tool_names = [t["name"] for t in tools] | ||
| assert "included" in tool_names | ||
| assert "excluded" not in tool_names | ||
|
|
||
| resources = _mcp_method(dash_duo.server.url, "resources/list") | ||
| uris = [r["uri"] for r in resources["result"]["resources"]] | ||
| assert "dash://layout" in uris |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.