Skip to content

MCP Server Part 8: MCP server routes - #3750

Merged
KoolADE85 merged 17 commits into
mcpfrom
feature/mcp-server-integration
May 19, 2026
Merged

MCP Server Part 8: MCP server routes#3750
KoolADE85 merged 17 commits into
mcpfrom
feature/mcp-server-integration

Conversation

@KoolADE85

@KoolADE85KoolADE85 commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR pulls together everything from the previous PRs into a working MCP server you can connect to from any MCP-compatible LLM agent. Example:

claude mcp add dash-app --transport http http://localhost:8050/_mcp
  • Routes are added per MCP spec
  • Dash integration: control the feature with enable_mcp and mcp_path constructor params, (or DASH_MCP_ENABLED / DASH_MCP_PATH env vars)
  • E2E test suite: all the previous PRs have E2E coverage via the actual MCP server endpoints.
  • A number of one-liner type fixes since the mypy tests are now running in this PR.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Thank you for your contribution to Dash! 🎉

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

@chgiesse

Copy link
Copy Markdown
Contributor

Hi Plotly team!

First of all, thanks for integrating MCP into Dash - this looks really exciting!
But I would like to address/discuss some concerns:

  • this PR looks like its not compatible with the new bring your own Backend architecture by directly importing components like Response from Flask and not the response adapter - thus the mcp doesn't work with FastApi or Quart
  • the same seems to apply for the SSE streaming. Which I think should be integrated into the server adapter classes and be provided for the other backends too
  • and regarding the SSE implementation - I think the this version has some serious security implications - for synchronous Flask. With the current endless loop, session wise handling and Gunicorns default synchronous workers, the life time per session per sse stream blocks a complete worker! For example, google Chrome doesn't really provide a tab limit, so I could open a 1000 tabs and block a 1000 workers in your application. Although I think there is not a direct solutions to this (besides not providing endless stream capabilities to a synchronous backend) - a way to handle this would be to provide a DashQueueAdapter where a user can use a Gevent Queue instead of the default one. With this, the problem can be smoothed out in production. I also created a blog post ones - comparing server performance with SSE streams between Flask and Quart. https://community.plotly.com/t/flash-1-2-0-streaming-ui-updates-via-server-sent-events/93890/2

Let me know what you think - if you see it the same way or if I overlooked something! Would also love to contribute these changes :)

Best regards,
Christian

@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from 8c1f392 to f9b05daCompareApril 21, 2026 17:28
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 43c15b2 to f03c72fCompareApril 21, 2026 17:50
@KoolADE85

Copy link
Copy Markdown
ContributorAuthor

Hey @chgiesse

Thanks for the comments, and happy to see the excitement around the feature!

  • I'll be adjusting this to be compatible with the "bring your own backend" initiative. As you can see, the two projects have been happening independently so I'll make the appropriate code adjustments here.

  • As for SSE streaming, I'm thinking to remove that for the initial release. Early on, I thought it would be useful to have some server-driven messaging, and this would represent the start of that. But since the core functionality is all achieved with basic, well-described tools/resources, it's probably wise to remove SSE until we think through the security and performance considerations.

And absolutely, I'd welcome any contributions you have towards this effort - there's tons of potential to make Dash apps even more LLM friendly!

Adrian

@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from f9b05da to bc340e6CompareApril 22, 2026 21:37
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch 2 times, most recently from d3e1e19 to 976e0dfCompareApril 23, 2026 15:29
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from bc340e6 to b3d4015CompareApril 23, 2026 20:24
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 20d3ee7 to bb25991CompareApril 23, 2026 20:49
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from b3d4015 to e407569CompareApril 30, 2026 15:24
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from bb25991 to 9cc9959CompareApril 30, 2026 15:39
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from e407569 to 37e92f0CompareApril 30, 2026 16:02
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 9cc9959 to e84d208CompareApril 30, 2026 16:03
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from 37e92f0 to 28d5fedCompareApril 30, 2026 16:45
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from e84d208 to 943d9b2CompareApril 30, 2026 16:48
@chgiesse

chgiesse commented May 5, 2026

Copy link
Copy Markdown
Contributor

Hi @KoolADE85

Thanks for the response and making it compatible! Looking forward to use it!

Yes I also think that the functionality without sse is already a good starting point. Maybe providing it together with a native sse callback? :)

@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from 28d5fed to f098f88CompareMay 6, 2026 21:35
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from ac43cd0 to 8d7b8baCompareMay 6, 2026 21:37
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from f098f88 to 79df12eCompareMay 8, 2026 15:40
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch 2 times, most recently from e0662fc to 3c39eacCompareMay 8, 2026 21:56
@KoolADE85
KoolADE85force-pushed the feature/mcp-get-dash-component-tool branch from f1b6cc2 to d25123bCompareMay 8, 2026 22:04
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 3c39eac to a1ca057CompareMay 8, 2026 22:10
@KoolADE85
KoolADE85 changed the base branch from feature/mcp-get-dash-component-tool to mcpMay 11, 2026 23:18
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from a1ca057 to 409be55CompareMay 11, 2026 23:19
[
{
"id": str(comp.id),
"id": str(getattr(comp, "id", 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.

Doesn't line 49 guarantee that id exists?

Comment on lines +70 to +72
if pattern is None:
return None
matches = find_matching_components(pattern)

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.

Suggested change
ifpatternisNone:
returnNone
matches=find_matching_components(pattern)
matches=find_matching_components(pattern) ifpatternelseNone

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This one was actually necessary for the linter too.

dep: CallbackDependency, value: Any
) -> CallbackInput | list[CallbackInput]:
def _expand_dep(dep: CallbackDependency, value: Any) -> CallbackInputs:
"""Attach a concrete value to a callback dependency to produce a valid callback input.

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.

Claude 😠!

Suggested change
"""Attachaconcretevaluetoacallbackdependencytoproduceavalidcallbackinput.
"""
Attachaconcretevaluetoacallbackdependencytoproduceavalidcallbackinput.

concrete_ids = [comp.id for comp in find_matching_components(pattern)]
expanded = [{"id": cid, "property": prop} for cid in concrete_ids]
concrete_ids = [
getattr(comp, "id") for comp in find_matching_components(pattern)

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 comp be None here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It cannot be None since find_matching_components only returns pattern-matching components (i.e. id is a dict), so getattr(comp, "id") is always a dict.

Comment on lines +49 to +51
if func is not None:
return _wrap(func)
return _wrap

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.

Do you need to return the wrapping function here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is because the decorator supports optional arguments:
func is None when used with arguments (@mcp_enabled(name="foo"))
It's not None when used bare (@mcp_enabled)

Comment threaddash/mcp/_server.py Outdated
app.mcp_decorated_functions = dict(MCP_DECORATED_FUNCTIONS)
MCP_DECORATED_FUNCTIONS.clear()

_session_id: str | None = 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.

Could you look into what would happen to this value if multiple users attempt to connect at the same time? Would some users end up with the wrong session id? It's a classic TOCTOU race condition 🙄.

Could multiple gunicorn works get there own session id? You mentioned this during our review.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is now addressed in dadc63d

@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 5df8628 to 7f06fd7CompareMay 14, 2026 15:06
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 750c20e to ca9146eCompareMay 14, 2026 16:11
@KoolADE85
KoolADE85 merged commit 465a368 into mcpMay 19, 2026
29 checks passed
Sign up for freeto 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

@KoolADE85@chgiesse@camdecoster