Skip to content

MCP Server Part 2: Represent callback-related data with a CallbackAdapter#3711

Open
KoolADE85 wants to merge 3 commits intomcpfrom
feature/mcp-callback-adapters
Open

MCP Server Part 2: Represent callback-related data with a CallbackAdapter#3711
KoolADE85 wants to merge 3 commits intomcpfrom
feature/mcp-callback-adapters

Conversation

@KoolADE85
Copy link
Copy Markdown
Contributor

@KoolADE85 KoolADE85 commented Apr 1, 2026

Summary

  • Add CallbackAdapter: wraps a Dash callback and exposes MCP-related metadata (computed lazily)
    • It combines Dash's own callback map, layout, and user docstrings/types into 1 interface that the MCP server will use to present a complete picture of the app to LLMs.
  • Add CallbackAdapterCollection for working with collections of adapters
  • Add mcp_enabled parameter to @app.callback() for opting callbacks out of MCP
  • Add mcp>=1.0.0 to dependencies

Manual testing

  • You can inspect what kind of data the CallbackAdapter exposes
  • you can run callbacks directly with run_callback (which is what MCP will do)
from dash import Dash, html, dcc, Input, Output
from dash._get_app import app_context

app = Dash(__name__)
app.layout = html.Div([
    dcc.Input(id="name", value="World"),
    html.Div(id="greeting"),
])
@app.callback(Output("greeting", "children"), Input("name", "value"))
def greet(name):
    """Greet the user by name."""
    return f"Hello, {name}!"

with app.server.app_context():
    app_context.set(app)
    from dash.mcp.primitives.tools.callback_adapter_collection import CallbackAdapterCollection
    from dash.mcp.primitives.tools.callback_utils import run_callback
    collection = CallbackAdapterCollection(app)
    for adapter in collection:
        print(f"Tool: {adapter.tool_name}")
        print(f"  Docstring: {adapter._docstring}")
        print(f"  Outputs: {[o['id_and_prop'] for o in adapter.outputs]}")
        print(f"  Inputs:  {[i['name'] for i in adapter.inputs]}")

    # Execute the callback through the adapter
    adapter = collection[0]
    result = run_callback(adapter, {"name": "Alice"})
    print(f"Callback result: {result}")

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Thank you for your contribution to Dash! 🎉

This PR is exempt from requiring a linked issue due to its labels.

@KoolADE85 KoolADE85 force-pushed the feature/mcp-callback-adapters branch 2 times, most recently from 42aa12d to 0334a96 Compare April 1, 2026 22:45
@KoolADE85 KoolADE85 force-pushed the feature/mcp-callback-adapters branch 2 times, most recently from c6cbe97 to cd073cf Compare April 6, 2026 18:11
@KoolADE85 KoolADE85 changed the base branch from feature/mcp-framework-foundations to mcp April 8, 2026 15:57
@KoolADE85 KoolADE85 force-pushed the feature/mcp-callback-adapters branch from cd073cf to 74cd477 Compare April 8, 2026 15:59
Comment on lines +24 to +31
from dash.mcp.types import is_nullable
from dash._grouping import flatten_grouping
from dash._utils import clean_property_name, split_callback_id
from dash.mcp.types import MCPInput, MCPOutput
from .callback_utils import run_callback
from .descriptions import build_tool_description
from .input_schemas import get_input_schema
from .output_schemas import get_output_schema
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dash.mcp.types appears two times and there is a mix of absolute vs relative imports.

return [hints.get(func_name) for func_name, _ in self._dep_param_map]


def _expand_dep(dep: dict, value: Any) -> Any:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a type to this return value.

return {**dep, "value": value}


def _expand_output_spec(output_id: str, cb_info: dict, resolved_inputs: list) -> Any:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a type to this return value.

Comment on lines +400 to +402
if len(results) == 1:
return results[0]
return results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think returning the array and handling the results length at call site would be better since this makes the return value an union.


def _derive_output_ids(
output_pattern: dict, resolved_inputs: list
) -> list[dict] | None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add type definition to the dict if possible.

return None


def _coerce_value(value: Any) -> Any:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a generic here [TInput, TOutput]

class CallbackExecutionError(MCPError):
"""Callback raised an exception during execution."""

code = -32603
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these code? This one has the same code as the base MCPError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants