Skip to content

Commit 2b84e63

Browse files
committed
Refactor tool descriptions/schemas to use a base class (just as resources do)
1 parent 366d667 commit 2b84e63

14 files changed

Lines changed: 267 additions & 181 deletions
Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tool-level description generation for MCP tools.
22
3-
Each source shares the same signature:
4-
``(adapter: CallbackAdapter) -> list[str]``
3+
Each source is a ``ToolDescriptionSource`` subclass that can add text
4+
to the tool's description. All sources are accumulated.
55
66
This is distinct from per-parameter descriptions
77
(in ``input_schemas/input_descriptions/``) which populate
@@ -10,25 +10,24 @@
1010

1111
from __future__ import annotations
1212

13-
from __future__ import annotations
14-
1513
from typing import TYPE_CHECKING
1614

17-
from .description_docstring import callback_docstring
18-
from .description_outputs import output_summary
15+
from .base import ToolDescriptionSource
16+
from .description_docstring import DocstringDescription
17+
from .description_outputs import OutputSummaryDescription
1918

2019
if TYPE_CHECKING:
2120
from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter
2221

23-
_SOURCES = [
24-
output_summary,
25-
callback_docstring,
22+
_SOURCES: list[type[ToolDescriptionSource]] = [
23+
OutputSummaryDescription,
24+
DocstringDescription,
2625
]
2726

2827

29-
def build_tool_description(adapter: CallbackAdapter) -> str:
28+
def build_tool_description(callback: CallbackAdapter) -> str:
3029
"""Build a human-readable description for an MCP tool."""
3130
lines: list[str] = []
3231
for source in _SOURCES:
33-
lines.extend(source(adapter))
32+
lines.extend(source.describe(callback))
3433
return "\n".join(lines) if lines else "Dash callback"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Base class for tool-level description sources."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter
9+
10+
11+
class ToolDescriptionSource:
12+
"""A source of text that can describe an MCP tool.
13+
14+
Subclasses implement ``describe`` to return strings that will be
15+
joined into the tool's ``description`` field. All sources are
16+
accumulated — every source can add text to the overall description.
17+
"""
18+
19+
@classmethod
20+
def describe(cls, callback: CallbackAdapter) -> list[str]:
21+
raise NotImplementedError

dash/mcp/primitives/tools/descriptions/description_docstring.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
from typing import TYPE_CHECKING
66

7+
from .base import ToolDescriptionSource
8+
79
if TYPE_CHECKING:
810
from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter
911

1012

11-
def callback_docstring(adapter: CallbackAdapter) -> list[str]:
13+
class DocstringDescription(ToolDescriptionSource):
1214
"""Return the callback's docstring as description lines."""
13-
docstring = adapter._docstring
14-
if docstring:
15-
return ["", docstring.strip()]
16-
return []
15+
16+
@classmethod
17+
def describe(cls, callback: CallbackAdapter) -> list[str]:
18+
docstring = callback._docstring
19+
if docstring:
20+
return ["", docstring.strip()]
21+
return []

dash/mcp/primitives/tools/descriptions/description_outputs.py

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,53 @@
44

55
from typing import TYPE_CHECKING
66

7+
from .base import ToolDescriptionSource
8+
79
if TYPE_CHECKING:
810
from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter
911

1012
_OUTPUT_SEMANTICS: dict[tuple[str | None, str], str] = {
11-
("Graph", "figure"): "Returns chart/visualization data",
1213
("DataTable", "data"): "Returns tabular data",
1314
("DataTable", "columns"): "Returns table column definitions",
14-
("Dropdown", "options"): "Returns selection options",
15-
("Dropdown", "value"): "Updates a selection value",
16-
("RadioItems", "options"): "Returns selection options",
17-
("Checklist", "options"): "Returns selection options",
18-
("Store", "data"): "Returns stored data",
15+
("Store", "data"): "Returns data to be remembered client-side",
1916
("Download", "data"): "Returns downloadable content",
2017
("Markdown", "children"): "Returns formatted text",
2118
(None, "figure"): "Returns chart/visualization data",
22-
(None, "data"): "Returns data",
23-
(None, "options"): "Returns selection options",
19+
(None, "options"): "Returns available options",
2420
(None, "columns"): "Returns column definitions",
2521
(None, "children"): "Returns content",
26-
(None, "value"): "Returns a value",
22+
(None, "value"): "Returns the current value",
2723
(None, "style"): "Updates styling",
2824
(None, "disabled"): "Updates enabled/disabled state",
2925
}
3026

3127

32-
def output_summary(adapter: CallbackAdapter) -> list[str]:
28+
class OutputSummaryDescription(ToolDescriptionSource):
3329
"""Produce a short summary of what the callback outputs represent."""
34-
outputs = adapter.outputs
35-
if not outputs:
36-
return ["Dash callback"]
37-
38-
lines: list[str] = []
39-
for out in outputs:
40-
comp_id = out["component_id"]
41-
prop = out["property"]
42-
comp_type = out.get("component_type")
43-
44-
semantic = _OUTPUT_SEMANTICS.get((comp_type, prop))
45-
if semantic is None:
46-
semantic = _OUTPUT_SEMANTICS.get((None, prop))
47-
48-
if semantic is not None:
49-
lines.append(f"- {comp_id}.{prop}: {semantic}")
50-
else:
51-
lines.append(f"- {comp_id}.{prop}")
52-
53-
n = len(outputs)
54-
if n == 1:
55-
return [lines[0].lstrip("- ")]
56-
header = f"Returns {n} output{'s' if n > 1 else ''}:"
57-
return [header] + lines
30+
31+
@classmethod
32+
def describe(cls, callback: CallbackAdapter) -> list[str]:
33+
outputs = callback.outputs
34+
if not outputs:
35+
return ["Dash callback"]
36+
37+
lines: list[str] = []
38+
for out in outputs:
39+
comp_id = out["component_id"]
40+
prop = out["property"]
41+
comp_type = out.get("component_type")
42+
43+
semantic = _OUTPUT_SEMANTICS.get((comp_type, prop))
44+
if semantic is None:
45+
semantic = _OUTPUT_SEMANTICS.get((None, prop))
46+
47+
if semantic is not None:
48+
lines.append(f"- {comp_id}.{prop}: {semantic}")
49+
else:
50+
lines.append(f"- {comp_id}.{prop}")
51+
52+
n = len(outputs)
53+
if n == 1:
54+
return [lines[0].lstrip("- ")]
55+
header = f"Returns {n} output{'s' if n > 1 else ''}:"
56+
return [header] + lines

dash/mcp/primitives/tools/input_schemas/__init__.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
"""Input schema generation for MCP tool inputSchema fields.
22
3-
Mirrors ``output_schemas/`` which generates ``outputSchema``.
4-
5-
Each source is tried in priority order. All share the same signature:
6-
``(param: MCPInput) -> dict | None``.
3+
Each source is an ``InputSchemaSource`` subclass that can type
4+
an input parameter. Sources are tried in priority order — first
5+
non-None wins.
76
"""
87

98
from __future__ import annotations
109

1110
from typing import Any
1211

1312
from dash.mcp.types import MCPInput
14-
from .schema_callback_type_annotations import annotation_to_schema
15-
from .schema_component_proptypes_overrides import get_override_schema
16-
from .schema_component_proptypes import get_component_prop_schema
13+
14+
from .base import InputSchemaSource
15+
from .schema_callback_type_annotations import AnnotationSchema
16+
from .schema_component_proptypes_overrides import OverrideSchema
17+
from .schema_component_proptypes import ComponentPropSchema
1718
from .input_descriptions import get_property_description
1819

19-
_SOURCES = [
20-
annotation_to_schema,
21-
get_override_schema,
22-
get_component_prop_schema,
20+
_SOURCES: list[type[InputSchemaSource]] = [
21+
AnnotationSchema,
22+
OverrideSchema,
23+
ComponentPropSchema,
2324
]
2425

2526

@@ -31,7 +32,7 @@ def get_input_schema(param: MCPInput) -> dict[str, Any]:
3132
"""
3233
schema: dict[str, Any] = {}
3334
for source in _SOURCES:
34-
result = source(param)
35+
result = source.get_schema(param)
3536
if result is not None:
3637
schema = result
3738
break
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Base class for input schema sources."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from dash.mcp.types import MCPInput
8+
9+
10+
class InputSchemaSource:
11+
"""A source of JSON Schema that can type an MCP tool input parameter.
12+
13+
Subclasses implement ``get_schema`` to return a JSON Schema dict
14+
for the parameter, or ``None`` if this source cannot determine the
15+
type. Sources are tried in priority order — first non-None wins.
16+
"""
17+
18+
@classmethod
19+
def get_schema(cls, param: MCPInput) -> dict[str, Any] | None:
20+
raise NotImplementedError
Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
"""Per-property description generation for MCP tool input parameters.
22
3-
Each source shares the same signature:
4-
``(param: MCPInput) -> list[str]``
5-
6-
Sources are tried in order from most generic to most instance-specific.
7-
All sources that produce lines are combined.
3+
Each source is an ``InputDescriptionSource`` subclass that can add
4+
text to a parameter's description. All sources are accumulated.
85
"""
96

107
from __future__ import annotations
118

129
from dash.mcp.types import MCPInput
13-
from .description_component_props import component_props_description
14-
from .description_docstrings import docstring_prop_description
15-
from .description_html_labels import label_description
1610

17-
_SOURCES = [
18-
docstring_prop_description,
19-
label_description,
20-
component_props_description,
11+
from .base import InputDescriptionSource
12+
from .description_component_props import ComponentPropsDescription
13+
from .description_docstrings import DocstringPropDescription
14+
from .description_html_labels import LabelDescription
15+
16+
_SOURCES: list[type[InputDescriptionSource]] = [
17+
DocstringPropDescription,
18+
LabelDescription,
19+
ComponentPropsDescription,
2120
]
2221

2322

@@ -27,5 +26,5 @@ def get_property_description(param: MCPInput) -> str | None:
2726
if not param.get("required", True):
2827
lines.append("Input is optional.")
2928
for source in _SOURCES:
30-
lines.extend(source(param))
29+
lines.extend(source.describe(param))
3130
return "\n".join(lines) if lines else None
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Base class for per-parameter description sources."""
2+
3+
from __future__ import annotations
4+
5+
from dash.mcp.types import MCPInput
6+
7+
8+
class InputDescriptionSource:
9+
"""A source of text that can describe an MCP tool input parameter.
10+
11+
Subclasses implement ``describe`` to return strings that will be
12+
added to the callback parameter's description. All sources
13+
are accumulated — every source can add text to the overall description.
14+
"""
15+
16+
@classmethod
17+
def describe(cls, param: MCPInput) -> list[str]:
18+
raise NotImplementedError

0 commit comments

Comments
 (0)