-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
MCP Server Part 4: Expose callbacks as tools #3731
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
Open
KoolADE85
wants to merge
7
commits into
mcp
Choose a base branch
from
feature/mcp-tools
base: mcp
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
eddf1b2
Implement callbacks as tools with rich input/output schema and descri…
KoolADE85 df757d5
Refactor description sources to accept CallbackAdapter instances
KoolADE85 3f8a0f0
Fix pylint error
KoolADE85 3a1c64c
Fix regression in CallbackAdapter
KoolADE85 e62b7ca
Refactor tool descriptions/schemas to use a base class (just as resou…
KoolADE85 37edeec
Disable docstrings by default in MCP tool descriptions
KoolADE85 c6187b5
lint
KoolADE85 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,33 @@ | ||
| """Stub — real implementation in a later PR.""" | ||
| """Tool-level description generation for MCP tools. | ||
|
|
||
| Each source is a ``ToolDescriptionSource`` subclass that can add text | ||
| to the tool's description. All sources are accumulated. | ||
|
|
||
| def build_tool_description(outputs, docstring=None): # pylint: disable=unused-argument | ||
| if docstring: | ||
| return docstring.strip() | ||
| return "Dash callback" | ||
| This is distinct from per-parameter descriptions | ||
| (in ``input_schemas/input_descriptions/``) which populate | ||
| ``inputSchema.properties.{param}.description``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from .base import ToolDescriptionSource | ||
| from .description_docstring import DocstringDescription | ||
| from .description_outputs import OutputSummaryDescription | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter | ||
|
|
||
| _SOURCES: list[type[ToolDescriptionSource]] = [ | ||
| OutputSummaryDescription, | ||
| DocstringDescription, | ||
| ] | ||
|
|
||
|
|
||
| def build_tool_description(callback: CallbackAdapter) -> str: | ||
| """Build a human-readable description for an MCP tool.""" | ||
| lines: list[str] = [] | ||
| for source in _SOURCES: | ||
| lines.extend(source.describe(callback)) | ||
| return "\n".join(lines) if lines else "Dash callback" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """Base class for tool-level description sources.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter | ||
|
|
||
|
|
||
| class ToolDescriptionSource: | ||
| """A source of text that can describe an MCP tool. | ||
|
|
||
| Subclasses implement ``describe`` to return strings that will be | ||
| joined into the tool's ``description`` field. All sources are | ||
| accumulated — every source can add text to the overall description. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def describe(cls, callback: CallbackAdapter) -> list[str]: | ||
| raise NotImplementedError |
39 changes: 39 additions & 0 deletions
39
dash/mcp/primitives/tools/descriptions/description_docstring.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """Callback docstring for tool descriptions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from dash import get_app | ||
|
|
||
| from .base import ToolDescriptionSource | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter | ||
|
|
||
|
|
||
| class DocstringDescription(ToolDescriptionSource): | ||
| """Return the callback's docstring as description lines. | ||
|
|
||
| Gated behind an opt-in flag: docstrings may contain sensitive | ||
| implementation details that the browser never surfaces to users, | ||
| so we don't expose them to MCP clients unless the author opts in | ||
| — either per-callback or app-wide. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def describe(cls, callback: CallbackAdapter) -> list[str]: | ||
| if not cls._is_exposed(callback): | ||
| return [] | ||
| docstring = callback._docstring # pylint: disable=protected-access | ||
| if docstring: | ||
| return ["", docstring.strip()] | ||
| return [] | ||
|
|
||
| @classmethod | ||
| def _is_exposed(cls, callback: CallbackAdapter) -> bool: | ||
| # pylint: disable-next=protected-access | ||
| per_callback = callback._cb_info.get("mcp_expose_docstring") | ||
| if per_callback is not None: | ||
| return per_callback | ||
| return get_app().config.get("mcp_expose_docstrings", False) |
56 changes: 56 additions & 0 deletions
56
dash/mcp/primitives/tools/descriptions/description_outputs.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Output summary for tool descriptions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from .base import ToolDescriptionSource | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter | ||
|
|
||
| _OUTPUT_SEMANTICS: dict[tuple[str | None, str], str] = { | ||
| ("DataTable", "data"): "Returns tabular data", | ||
| ("DataTable", "columns"): "Returns table column definitions", | ||
| ("Store", "data"): "Returns data to be remembered client-side", | ||
| ("Download", "data"): "Returns downloadable content", | ||
| ("Markdown", "children"): "Returns formatted text", | ||
| (None, "figure"): "Returns chart/visualization data", | ||
| (None, "options"): "Returns available options", | ||
| (None, "columns"): "Returns column definitions", | ||
| (None, "children"): "Returns content", | ||
| (None, "value"): "Returns the current value", | ||
| (None, "style"): "Updates styling", | ||
| (None, "disabled"): "Updates enabled/disabled state", | ||
| } | ||
|
|
||
|
|
||
| class OutputSummaryDescription(ToolDescriptionSource): | ||
| """Produce a short summary of what the callback outputs represent.""" | ||
|
|
||
| @classmethod | ||
| def describe(cls, callback: CallbackAdapter) -> list[str]: | ||
| outputs = callback.outputs | ||
| if not outputs: | ||
| return ["Dash callback"] | ||
|
|
||
| lines: list[str] = [] | ||
| for out in outputs: | ||
| comp_id = out["component_id"] | ||
| prop = out["property"] | ||
| comp_type = out.get("component_type") | ||
|
|
||
| semantic = _OUTPUT_SEMANTICS.get((comp_type, prop)) | ||
| if semantic is None: | ||
| semantic = _OUTPUT_SEMANTICS.get((None, prop)) | ||
|
|
||
| if semantic is not None: | ||
| lines.append(f"- {comp_id}.{prop}: {semantic}") | ||
| else: | ||
| lines.append(f"- {comp_id}.{prop}") | ||
|
|
||
| n = len(outputs) | ||
| if n == 1: | ||
| return [lines[0].lstrip("- ")] | ||
| header = f"Returns {n} output{'s' if n > 1 else ''}:" | ||
| return [header] + lines |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,44 @@ | ||
| """Stub — real implementation in a later PR.""" | ||
| """Input schema generation for MCP tool inputSchema fields. | ||
|
|
||
| Each source is an ``InputSchemaSource`` subclass that can type | ||
| an input parameter. Sources are tried in priority order — first | ||
| non-None wins. | ||
| """ | ||
|
|
||
| def get_input_schema(param): # pylint: disable=unused-argument | ||
| return {} | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from dash.mcp.types import MCPInput | ||
|
|
||
| from .base import InputSchemaSource | ||
| from .schema_callback_type_annotations import AnnotationSchema | ||
| from .schema_component_proptypes_overrides import OverrideSchema | ||
| from .schema_component_proptypes import ComponentPropSchema | ||
| from .input_descriptions import get_property_description | ||
|
|
||
| _SOURCES: list[type[InputSchemaSource]] = [ | ||
| AnnotationSchema, | ||
| OverrideSchema, | ||
| ComponentPropSchema, | ||
| ] | ||
|
|
||
|
|
||
| def get_input_schema(param: MCPInput) -> dict[str, Any]: | ||
| """Return the complete JSON Schema for a callback input parameter. | ||
|
|
||
| Type sources provide ``type``/``enum`` (first non-None wins). | ||
| Description is assembled by ``input_descriptions``. | ||
| """ | ||
| schema: dict[str, Any] = {} | ||
| for source in _SOURCES: | ||
| result = source.get_schema(param) | ||
| if result is not None: | ||
| schema = result | ||
| break | ||
|
|
||
| description = get_property_description(param) | ||
| if description: | ||
| schema = {**schema, "description": description} | ||
|
|
||
| return schema |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| """Base class for input schema sources.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from dash.mcp.types import MCPInput | ||
|
|
||
|
|
||
| class InputSchemaSource: | ||
| """A source of JSON Schema that can type an MCP tool input parameter. | ||
|
|
||
| Subclasses implement ``get_schema`` to return a JSON Schema dict | ||
| for the parameter, or ``None`` if this source cannot determine the | ||
| type. Sources are tried in priority order — first non-None wins. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def get_schema(cls, param: MCPInput) -> dict[str, Any] | None: | ||
| raise NotImplementedError |
30 changes: 30 additions & 0 deletions
30
dash/mcp/primitives/tools/input_schemas/input_descriptions/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| """Per-property description generation for MCP tool input parameters. | ||
|
|
||
| Each source is an ``InputDescriptionSource`` subclass that can add | ||
| text to a parameter's description. All sources are accumulated. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dash.mcp.types import MCPInput | ||
|
|
||
| from .base import InputDescriptionSource | ||
| from .description_component_props import ComponentPropsDescription | ||
| from .description_docstrings import DocstringPropDescription | ||
| from .description_html_labels import LabelDescription | ||
|
|
||
| _SOURCES: list[type[InputDescriptionSource]] = [ | ||
| DocstringPropDescription, | ||
| LabelDescription, | ||
| ComponentPropsDescription, | ||
| ] | ||
|
|
||
|
|
||
| def get_property_description(param: MCPInput) -> str | None: | ||
| """Build a complete description string for a callback input parameter.""" | ||
| lines: list[str] = [] | ||
| if not param.get("required", True): | ||
| lines.append("Input is optional.") | ||
| for source in _SOURCES: | ||
| lines.extend(source.describe(param)) | ||
| return "\n".join(lines) if lines else None |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Maybe out of scope of this PR, but are we really running 3.8? min should be 3.9
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.
yeah, all the CI tests are in 3.8 still.
We should definitely consider bumping the min version. The
mcppackage requires>=3.10so if we can make that the min version, we would be able to run all tests without any conditions.I also considered that out of scope for this PR, but maybe worth revisiting?